Django+Vue开发一个接口自动化平台
1:数据结构设计
系统设计:
1:主要功能 是什么
2:数据存放在数据库,Django使用 ORM操作数据库(数据字段,表之间的关系)设计数据结构
1个web系统,最核心的当属数据库结构了,因为一切都围绕数据计算开始。 接口测试平台,最核心的除了接口运行框架就数数据结构了,因为我们需要将用例的数据持久化到数据库中,方便读取修改与存储。 如果我们要以 httprunner 为接口用例运行框架,则需要按照期yaml/json的数据结构来设计数据库。 经过设计,总结出了一套数据库模型关系图的总体结构
![](https://img2023.cnblogs.com/blog/2070357/202302/2070357-20230215110809389-100245105.png)
HR测试用例结构:
config
......
teststep:一个测试步骤
request
......
这些 HR 数据都没必要放在一个表里
测试步骤可以引用测试用例,数据放一张表里系统可能存在臃余的数据,不方便
测试用例和测试步骤进行拆分,测试步骤没有必要和测试用例写在一张表里----结构拆分
HR 信息 存入数据库里,HR 目前写在 yaml文件里,不便于存在和修改,所以需要转化存储在系统里面 数据存放在数据库里
HR 用例的数据 放到数据库里
一个HR 用例:
用例主体:一个 case 专门存放用例主体,可以引用 config 或者 teststeps 存放关联用例
用例里 具体数据 config teststeps
一个测试用例可以关联多个 步骤
步骤里 嵌套结构 又比较深,有 request,request可以单独拿出来,请求数据,
其他的 extract,vaidate等都放在 teststeps里面
多个步骤 对应 一个用例 步骤外键指向 case
步骤可以指向某个用例,一个 teststep 可以指向用例,那么就不需要配置request, 被指向的用例 这个用例可以被步骤1指向,也可以被步骤2指向,可以被其他测试用例的步骤指向
测试步骤没有请求,单纯指向测试用例 也可以,测试用例可以被多个测试步骤所引用
步骤:多 测试用例:1 多对1
一个步骤不能引用多条用例,只能引用一条用例
请求和步骤:1 对 1的关系 互为引用,一个步骤对应一个 request
一个请求对应一个步骤
一个步骤 包含多个请求 那么这个步骤 引入的就是 其他测试用例 这样来实现的
在模型中定义了 OneToOneField ,该模型的实例只需通过其属 性就能访问关联对象
用例 和 配置:config 一对一 关系
case 管理测试用例。后面测试任务引用测试用例
case独立出来的一个数据结构,
数据解耦,config和case不放在一起,方便数据扩充,只需要1 对 1 关系 配置,config 类似 case的 一个扩展
1 对 1 关系 就是原模型的扩展:类似 person人 姓名,身高有这些基本数据,加一个address信息(包含国家,省,市,等具体信息) address可以作为一个单独的数据结构,一张单独的表
一个person只有一个address,address信息放在person看上去person显得臃肿,这时候把这块address信息独立出来,和原模型的关系 1 对 1
1 对 1 关系就是原模型的扩展
1对1关系:
模型中用到了两个知识点:
1对1关系和json字段,我们先看下1对1关系
从概念上理解,1对1就是1个模型关联另一个模型,他们之间的关系是1对1,这里可以理解为模型关系的扩展。
一对一关联 与 多对一关联 非常类似。若在模型中定义了 OneToOneField ,该模型的实例只需通过其属 性就能访问关联对象。
例如:
class EntryDetail(models.Model): entry = models.OneToOneField(Entry, on_delete=models.CASCADE) details = models.TextField() ed = EntryDetail.objects.get(id=2) ed.entry # Returns the related Entry object.
不同点在于 “反向” 查询。一对一关联 所关联的对象也能访问 Manager 对象,但这个 Manager 仅代表 一个对象,而不是对象的集合(QuerySet):
e = Entry.objects.get(id=2)
e.entrydetail # returns the related EntryDetail object
若未为关联关系指定对象,Django 会抛出 DoesNotExist 异常
实例 能通过 为 正向关联指定关联 对象一样的方式指定给反向关联:
e.entrydetail = ed
2:模型字段设计
Django创建一个项目
0:C:\Users\Autel\Desktop\course_django> 进入虚拟目录
1:djenv\Scripts\activate.bat 激活寻环境
2:django-admin startproject autotpsite 创建一个Django项目 名字叫 autospite
3:(djenv) C:\Users\Autel\Desktop\course_django>cd autotpsite 进入新创建的Django项目
4:django-admin startapp sqtp 创建一个应用程序 sqtp
5:pycham 打开 Django项目 在 autotpsite 项目下编码
1:autotpsite - settings.py 注册创建的应用程序 sqtp:INSTALLED_APPS 需要自己注册
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'sqtp' ]
2:sqtp的 models.py 文件中 编写模型代码
# autotpsite ——> sqtp ——> models.py from django.db import models # 测试平台核心模型--拆解HR用例部分 class Config(models.Model): """ 根据 HttpRunner config 字段设计 """ name = models.CharField('名称', max_length=128, unique=True) # 测试用例的名称,字符串类型 # 第一个参数 '名称' 是 verbose_name 表示这个字段,这个列,后面使用Django admin查看这个数据内容的时候 # 如果不写这个字段显示的是name名称,这个字段是对name这个字段名字的一种显示方法,没有其他特殊含义,展示使用,列的别名 # verbose_name='名称' 或者 放在第一个字段 这样定义 # max_length:最大长度 # unique=True:唯一,用例名称唯一 base_url = models.CharField('IP/域名', max_length=256, null=True, blank=True) # 可为空或空白 # 主机地址 # max_length:最大长度 # null:可以为空 # blank:空白,空的字符串也是可以的 # null 和 black空白:null是一个数据类型null,空白是一个空的字符串 variables = models.JSONField('变量', null=True) # sqlite不支持JSONField 数据类型,需要用到mysql # variables:里面的 值 存储的是一个字典类型 a = xxx # null=True 允许为空,json为null和 sql的null 有区别 parameters = models.JSONField('参数', null=True) # varA: [aaa, bbb] # parameters 里面也是一个字典,字典里面存储的 键值对,键值对里的值又是一个列表 parameters做数据驱动的 export = models.JSONField('用例返回值', null=True) # export里面的内容直接就是一个列表 [aaa, bbb, ccc] 导出的变量 # 当前用例返回值,其他用例引用这个测试用例的时候可以引用 export 这里导出的变量 verify = models.BooleanField('https校验', default=False) # default=False:默认False不校验HTTPS,设置True需要指定request请求的证书 def __str__(self): return self.name class Case(models.Model): """测试用例 case""" config = models.OneToOneField(Config, on_delete=models.DO_NOTHING) # 一对一的关联关系:指向 config # on_delete=models.DO_NOTHING 一对一关联关系删除了,config如果被删除了那么这个case 什么都不做,保留 # 用例可能被其他case引用 file_path = models.CharField('用例文件路径', max_length=1000, default='demo_case.json') # 用例文件路径,数据存储在数据库,需要把数据从数据库提取出来按照层级结构写到文件里,用例路径 # 用例路径存放在case这里,方便访问 # default:默认名称 demo_case.json 用例文件轻松转成json,转yaml需要多一个步骤 def __str__(self): return self.config.name # 用例名称和所在的目录做一个组合存储到 file_path 里,这里就是外键 Config 的名称 # config的名称就等同 用例的名称 class Step(models.Model): """定义测试步骤""" # 反向查询名称 同个模型中,两个以上字段关联同一个模型,必须指定related_name # 步骤属于那条用例 多对一 belong_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name='teststeps') # 用例被删除了,步骤就没有存在的必要,步骤也跟着删除就行 步骤无主存在没有什么必要 # 步骤引用的哪条用例 外键 指向 Case linked_case = models.ForeignKey(Case, on_delete=models.SET_NULL, null=True, related_name='linked_steps') # on_delete:引用的用例删除了,这个步骤存在还是有必要的 # 引用的用例删除了,这个步骤的数据结构还可以存在 # models.SET_NULL:表示引用的用例删除了,设置为null 允许为空 # 同一个模型有两个外键字段,并且两个外键字段指向同一个模型 name = models.CharField('名称', max_length=128) # 测试步骤同名很正常 variables = models.JSONField('变量', null=True) extract = models.JSONField('请求返回值', null=True) # 当前http响应中提取参数,提取当前步骤响应结果的 validate = models.JSONField('校验项', null=True) setup_hooks = models.JSONField('初始化', null=True) teardown_hooks = models.JSONField('清除', null=True) def __str__(self): return self.name class Request(models.Model): """把步骤中的 Request 提取出来了,request和测试步骤 一对一的,这个request和request的http请求库别冲突了---注意""" method_choices = ( # method可选的字段, (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True) # 关联步骤:models.CASCADE步骤删除了request也删除,请求和步骤属于扩展关系, # request已经独立出来了:可能步骤没有请求,链接了其他用例,可以设置为 null=True method = models.SmallIntegerField('请求方法', choices=method_choices, default=0) # 指定选项值:SmallIntegerField:整数类型,小范围整数节省内存空间 # 1:先创建一个二维元组 method_choices 如上 # 2:字段指定范围选择需要加上 choices 选项决定,指向 method_choices 二维元组 # default=0 默认值为 get 请求 url = models.CharField('请求路径', default='/', max_length=1000) # 可能请求路径就是根路径 所以 default='/' params = models.JSONField('url参数', null=True) headers = models.JSONField('请求头', null=True) cookies = models.JSONField('Cookies', null=True) data = models.JSONField('表单参数', null=True) json = models.JSONField('json参数', null=True) def __str__(self): return self.url
2:模型代码设计
其中,request,step和config参考HR对应部分的字段,由于其中会出现很多嵌套字段,所以1层的字段就用json数据类型来代替
class Config(models.Model): name = models.CharField('名称', max_length=128) base_url = models.CharField('IP/域名', max_length=512, null=True, blank=True) # 可Null,可空白 variables = models.JSONField('变量', null=True) parameters = models.JSONField('参数', null=True) # 用于参数化 verify = models.BooleanField('https校验', default=False) export = models.JSONField('用例返回值', null=True) def __str__(self): return self.name
class Step(models.Model): # 同个模型中,两个字段关联同1个模型,必须指定related_name,且名字不能相同 # 属于哪个用例 belong_case = models.ForeignKey('Case', on_delete=models.CASCADE, related_name='teststeps') # 引用哪条用例 linked_case = models.ForeignKey('Case', on_delete=models.SET_NULL, null=True, related_name='linked_steps') name = models.CharField('名称', max_length=128) variables = models.JSONField('变量', null=True) #默认sqlite数据库不支持json字段 extract = models.JSONField('请求返回值', null=True) validate = models.JSONField('校验项', null=True) setup_hooks = models.JSONField('初始化', null=True) teardown_hooks = models.JSONField('清除', null=True) def __str__(self): return self.name
# 请求 class Request(models.Model): method_choices = ( # method可选字段,二维元组 (0, 'GET'), # 参数1:保存在数据库中的值,参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True,related_name='testrequest') method = models.SmallIntegerField('请求方法', choices=method_choices, default=0) url = models.CharField('请求路径', default='/', max_length=1000) params = models.JSONField('url参数', null=True) headers = models.JSONField('请求头', null=True) cookies = models.JSONField('Cookies', null=True) data = models.JSONField('data参数', null=True) json = models.JSONField('json参数', null=True) def __str__(self): return self.url
此外,还需要创建case来统一管理接口用例 class Case(models.Model): config = models.OneToOneField(Config, on_delete=models.DO_NOTHING) suite = models.ForeignKey('Suite', on_delete=models.DO_NOTHING, null=True) file_path = models.CharField('用例文件路径', max_length=1000, default='demo_case.json') def __str__(self): return self.config.name
同步数据库:
python manage.py makemigrations
python manage.py migrate
2:ORM反向查询概念解析
正向查询
在1对1,1对多,多对多关系中都存在反向查询,若想知道什么是反向查询先了解什么是正向查询
模型查询其关联的项目叫做正向查询(外键定义在模型):
例如:步骤查询其所在的用例
#test.py class TestRelatedQuery(TestCase): def setUp(self) -> None: # 创建用例 self.config = Config.objects.create(name='用例1') self.case = Case.objects.create(config=self.config) def test_step_case(self): step1 = Step.objects.create(case=self.case,name='步骤1') step2 = Step.objects.create(case=self.case,name='步骤2') print(step1.case) # 正向查询 print(step2.case) # 正向查询
反向查询 反过来,项目查询下面的模型叫做反向查询,通过反向查询的结果QuerySet 方式1:
未指定related_name时 modelobj.field_set.all() 方式2:
指定related_name时 modelobj.related_name.all() 案例:sqtp/tests.py
class TestRelatedQuery(TestCase): def setUp(self) -> None: # 创建用例 self.config = Config.objects.create(name='用例1') self.suite_conf = Config.objects.create(name='套件') self.suite = Suite.objects.create(config=self.suite_conf) self.case = Case.objects.create(config=self.config,suite=self.suite) def test_step_case(self): step1 = Step.objects.create(case=self.case,name='步骤1') step2 = Step.objects.create(case=self.case,name='步骤2') print(step1.case) # 正向查询 print(step2.case) # 正向查询 print(self.case.teststeps.all()) # 反向查询--外键字段指定了related_name 【查询这个case用例有那些step步骤,步骤外键指向case】 print(self.suite.case_set.all()) # 反向查询--外键字段未指定related_name
1对1关系的反向查询: 此时反向查询的结果不是QuerySet,而是数据对象 print(self.config.case) # 1对1关系反向查询 print(self.suite_conf.suite) # 1对1关系反向查询
2:ORM之Json字段操作解析
由于模型中大部分字段都是json类型存储,之前我们未接触过,
所以需要对json字段的操作有一定了解以Request(请求信息)模型为例,操作增删改查
打开django shell:
python manage.py shell
新增:
json字段的值直接传字符串,列表,字典即可,(json对应的python数据类型) >>> req1=Request.objects.create(method=1,url='/example/demo',data={'name':'xiaoming','age':17,'addr':'nanjing'}) >>> req1.data {'name': 'xiaoming', 'age': 17, 'addr': 'nanjing'} >>> req2=Request.objects.create(method=1,url='/example/demo2',json='hello') >>> req2.json 'hello' >>> req3=Request.objects.create(method=1,url='/example/demo3',json=['a','b',1,2]) >>> req3.json ['a', 'b', 1, 2]
修改: 修改整体 >>> req2.json={'name': 'mike', 'age': 17, 'addr': 'nanjing'} >>> req2.save() >>> req2.json {'name': 'mike', 'age': 17, 'addr': 'nanjing'} 修改局部 >>> req1.data['name'] 'xiaoming' >>> req1.data['name']='mike' >>> req1.save() >>> req1.data {'name': 'mike', 'age': 17, 'addr': 'nanjing'}
删除: 删除整体: >>> from django.db.models import Value >>> req2.json=Value('null') # 存储为Json的null 删除局部: >>> req2.json.pop('name') 'mike' >>> req2.save() >>> req2.json {'age': 17, 'addr': 'nanjing'}
查询: >>> Request.objects.filter(json__age=17) <QuerySet [<Request: /example/demo2>]>
3:mysql数据库配置 安装 5.7 mysql版本
windows搭建mysql 参考文档:https://downloads.mysql.com/archives/community/
windows机器开启mysql 服务:
d:
cd D:\mysql-5.7.12-winx64\bin
net start mysql
windows机器关闭mysql 服务:
d:
cd D:\mysql-5.7.12-winx64\bin
net stop mysql
windows机器进入mysql命令行:
mysql -u root -p
卸载MySQL :
mysqld --remove mysql
创建数据库:
不同于sqlite,数据选择mysql时 django 不会帮你创建数据库,只会关联已有的数据库
CREATE DATABASE `db_name` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 创建一个数据库
python 安装 pymysqlclient模块
pip install mysqlclient 安装Django操作Mysql的数据库驱动
Django对应的数据库参考配置如下: 项目/settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'course_autotp', #数据库名称 'USER': 'youruser', #用户名 'PASSWORD': 'youpsd', #密码 'HOST': '127.0.0.1', 'PORT': 'yourport', 'TEST':{ 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci', } } }
Django默认使用的是 sqlite3:如下 修改这个配置 autotpsite/autotpsite/settings.py 文件里修改
DATABASES = {'default':
{'ENGINE':
'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),}
}
DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } 'default': { 'ENGINE': 'django.db.backends.mysql', # 数据库引擎,Django自带的,底层依赖 mysqlclient 'NAME': 'autotpsite', # 数据库名称,mysql里数据库创建的库名称 手动创建数据库 'USER': 'root', # 用户名 'PASSWORD': '123456', # 密码 'HOST': '127.0.0.1', # 主机地址 'PORT': '3306', # 端口号 'TEST': { # 表示 单元 测试 创建的数据库配置,字符串编码 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci', } } }
同步数据库:
djenv\Scripts\activate.bat 激活寻环境
(djenv) C:\Users\Autel\Desktop\course_django\autotpsite> 进入 autotpsize项目目录下 python manager命令同步
python manage.py makemigrations
python manage.py migrate
现在数据库就同步完毕
同步数据库可能存在的问题:
1:同一个模型有两个外键字段,并且两个外键字段指向同一个模型 同步的时候会报错
解决方案:使用:related_name 指向反向查询名称:同个模型中,两个以上字段关联同一个模型,必须指定 related_name,并且名字不能相同 如下
class Step(models.Model): """定义测试步骤""" # 反向查询名称 同个模型中,两个以上字段关联同一个模型,必须指定related_name # 步骤属于那条用例 多对一 belong_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name='teststeps') # 用例被删除了,步骤就没有存在的必要,步骤也跟着删除就行 步骤无主存在没有什么必要 # 步骤引用的哪条用例 外键 指向 Case linked_case = models.ForeignKey(Case, on_delete=models.SET_NULL, null=True, related_name='linked_steps') # on_delete:引用的用例删除了,这个步骤存在还是有必要的 # 引用的用例删除了,这个步骤的数据结构还可以存在 # models.SET_NULL:表示引用的用例删除了,设置为null 允许为空 # 同一个模型有两个外键字段,并且两个外键字段指向同一个模型 # 同一个模型有两个外键字段,并且两个外键字段指向同一个模型 :正向查询通过 belong_case 和 linked_case就行 # 反向查询的时候 Case.step_set 这样查询获取到模型管理器 再 all等操作 返回queue——set数据类型 # 但是 Case.step_set这样反向查询 无法反向 指定 指向 belong_case 或者 linked_case 不知道 # 不知道反向查询是查询这条 case 下的 有哪些步骤 还是这条用例被哪些步骤所引用 无法查询出来 # 反向查询的时候 一个数据里多条外键 多个外键指向同一个模型 这个被指向的模型 做反向查询的时候 查询不出具体指向 # 使用:related_name 指向反向查询名称:同个模型中,两个以上字段关联同一个模型,必须指定 related_name,并且名字不能相同 # 后面反向查询不需要 Case.step_set 这样查询 直接 Case.related_name的名字去查询 name = models.CharField('名称', max_length=128) # 测试步骤同名很正常 variables = models.JSONField('变量', null=True) extract = models.JSONField('请求返回值', null=True) # 当前http响应中提取参数,提取当前步骤响应结果的 validate = models.JSONField('校验项', null=True) setup_hooks = models.JSONField('初始化', null=True) teardown_hooks = models.JSONField('清除', null=True) def __str__(self): return self.name # case在下面 class Case(models.Model): """测试用例 case""" config = models.OneToOneField(Config, on_delete=models.DO_NOTHING) # 一对一的关联关系:指向 config # on_delete=models.DO_NOTHING 一对一关联关系删除了,config如果被删除了那么这个case 什么都不做,保留 # 用例可能被其他case引用 file_path = models.CharField('用例文件路径', max_length=1000, default='demo_case.json') # 用例文件路径,数据存储在数据库,需要把数据从数据库提取出来按照层级结构写到文件里,用例路径 # 用例路径存放在case这里,方便访问 # default:默认名称 demo_case.json 用例文件轻松转成json,转yaml需要多一个步骤 def __str__(self): return self.config.name # 用例名称和所在的目录做一个组合存储到 file_path 里,这里就是外键 Config 的名称 # config的名称就等同 用例的名称
2:Django 3.1版本以下不支持 JSONField 数据类型 解决方案:https://blog.csdn.net/weixin_43914675/article/details/123840334
数据 同步 完成就能看到 4 张表: sqtp_config sqtp_Case sqtp_step sqtp_request 其他的表 都是 Django 自己创建的表
3:pymysql
除了mysqlclient,django操作mysql数据库依赖库还可以用 pymysql mysql链接时,python版本过高,Django版本低导致连接有问题
1.使用pymysql
pip install pymysql
2:将Django 安装到最新
3:将pymysql 伪装成MySQLdb
在主项目的init.py中写如下代码:
import pymysql
pymysql.install_as_MySQLdb()
4:将base.py中的报错信息注释掉,如果有此处错误就做这一步,没有就忽略
将Django 安装到最新
将pymysql 伪装成MySQLdb。
在主项目的init.py中写如下代码
import pymysql pymysql.install_as_MySQLdb()
4:将base.py中的报错信息注释掉,如果有此处错误就做这一步,没有就忽略
if version < (1, 3, 13): raise ImproperlyConfigured('mysqlclient 1.3.13 or newer is required; you have %s.' % Database.__version__)
5:在数据库中创建对应数据库
进入MySQL数据库
create database db_autotp;
6:在setting.py中配置数据库
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'db_autotp', 'USER': 'root', 'PASSWORD': 'devops', 'HOST': '192.168.21.140', 'PORT': '3306', 'TEST':{ 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci', } } }
7:生成迁移文件 :python manage.py makemigrations
再执行迁移文件:python manage.py migrate
8:使用pymysql连接数据库就成功了
4:Django ORM 之 正向查询 和 反向查询
(djenv) C:\Users\Autel\Desktop\course_django\autotpsite>python manage.py test Django执行 单元测试用例
正向查询:模型查询其关联的项目叫做正向查询(外键定义在模型):
步骤必须关联 一个 或者 多个用例
例如:步骤查询其所在的用例
反向查询:未 定义 外键 字段的模型 查找 其关联关系
反过来,项目查询下面的模型叫做反向查询,通过反向查询的结果QuerySet
方式1:未指定 related_name 时
modelobj.field_set.all()
方式2:指定 related_name 时
modelobj.related_name.all()
# 正向查询和反向查询的案例 from django.test import TestCase from sqtp.models import Case, Config, Step, Request class TestRelatedQuery(TestCase): def setUp(self) -> None: # 初始化:创建用例 config1 = Config.objects.create(name='case001', base_url='http://localhost') # 创建一个 config,用例必须有config config2 = Config.objects.create(name='case002', base_url='http://localhost') self.case1 = Case.objects.create(config=config1) # 创建一个 case 用例,case 需要指向一个引用,case和config是一对一指向关系 self.case2 = Case.objects.create(config=config2) def test_steps_query(self): step1 = Step.objects.create(belong_case=self.case1, name='step1') # 创建一个步骤:指定步骤属于那条用例,属于 case1用例 step1.linked_case = self.case2 step1.save() # 修改了 step1 关联的 case 还需要 save 保存 step2 = Step.objects.create(belong_case=self.case2, name='step2') # 正向查询 print('=============正向查询============') print(step1.belong_case) # 查看step1所属用例 step的外键字段 belong_case 关联了 case 某个测试用例 print(step1.linked_case) # 查看step1关联的用例 # 反向查询 print('=============反向查询============') # related_name代替step_set # print(self.case1.step_set.all()) # 以前传统的反向查询的写法现在报错:AttributeError: 'Case' object has no attribute 'step_set' # 这样写法现在不行了,当外键设置了 related_name 的时候不能使用原始的方式了 使用 原始方式 无法 确定查询的是关联的步骤还是引用他的步骤 # 这时候只能使用 指定的 related_name 反向查询名称 来反向查询 print(self.case1.teststeps) # print(self.case1.teststeps.all()) # 查询 case1 下面有哪些步骤 都是 step1 print(self.case2.linked_steps.all()) # 查询 case2 被哪些步骤引用 都是 step1
1对1关系的反向查询: print(self.config.case) # 1对1关系反向查询 print(self.suite_conf.suite) # 1对1关系反向查询
此时反向查询的结果不是QuerySet,而是数据对象
5:Json字段操作解析
模型中大部分字段都是json类型存储,所以需要对json字段的操作有一定了解
以Request(请求信息)模型为例,操作增删改查
打开django shell:python manage.py shell
from django.test import TestCase from django.db.models import Value from sqtp.models import Case, Config, Step, Request class TestJsonField(TestCase): def setUp(self) -> None: req1 = Request.objects.create(method=1, url='/mgr/course/', data={"name": "小明", "age": 16, "address": "nanjing"}) # request表 创建 一条记录 # json类型的数据传 字符串,数字,列表,字典都可以 def test_json01(self): req = Request.objects.all().first() # 查询 print(req) # 获取到这个 /mgr/course/ # 测试修改--整体 修改了 date这个数据的全部字段 req.data = {"name": "小强", "age": 18, "address": "shanghai", "school": {"name": "北大", "level": "top1"}} req.save() print(Request.objects.all().first().data) # 查看修改后的内容 # 修改局部 修改date 里面 某个 字段 req = Request.objects.all().first() print(req.data['name']) # 修改前 req.data['name'] = '星辰大海' req.save() # print(req.data['name']) # 修改后,这样查询不行,是从 对象 里 又获取了属性,需要知道数据库真实最新的数据需要 Request.objects.all().first()再查询一次 print(Request.objects.all().first().data['name']) # 修改后 # json删除操作 # 删除整体 # req.data=Value('null') 设置成json的null 需要导入:from django.db.models import Value # req.data=None 设置成sql的null # req.save() # Django判断字段是否为空这两种null结果不同 # print(Request.objects.all().first().data.is_null()) 判空方法 # print(Request.objects.all().first().data) # 删除局部:操作字典一样操作键值对 req.data.pop('name') # pop 弹出 req.save() print(Request.objects.all().first().data) # 条件查询--根据json字段的某个值来查询,比如查询 name 为 小强的, age为 18的数据 # 方法:json字段__嵌套字段 查询 data的某个字段 == 什么的 # res=Request.objects.filter(data__age=18) res = Request.objects.filter(data__school__name="北大", data__age=18) print('======json条件查询=====') print(res)
6:数据库查询强化训练:字段条件查询
字段条件查询:
字段查询是指如何指定SQL WHERE子句的内容。它们用作QuerySet的filter(), exclude()和get()方法的关 键字参数
其基本格式是:field__lookuptype=value,注意其中是双下划线
默认查找类型为exact(精确匹配)。
lookuptype的类型有:
Django的数据库API支持20多种查询类型,下表列出了所有的字段查询参数:
字段名 说明 exact 精确匹配 iexact 不区分大小写的精确匹配 contains 包含匹配 icontains 不区分大小写的包含匹配 in 在..之内的匹配 gt 大于 gte 大于等于 lt 小于 lte 小于等于 startswith 从开头匹配 istartswith 不区分大小写从开头匹配 endswith 从结尾处匹配 iendswith 不区分大小写从结尾处匹配 range 范围匹配 date 日期匹配 year 年份 iso_year 以ISO 8601标准确定的年份 month 月份 day 日期 week 第几周 week_day 周几 iso_week_day 以ISO 8601标准确定的星期几 quarter 季度 time 时间 hour 小时 minute 分钟 second 秒 regex 区分大小写的正则匹配 iregex 不区分大小写的正则匹配
# 字段条件查询 class TestFieldQuery(TestCase): def setUp(self) -> None: req1 = Request.objects.create(method=1, url='/mgr/course/', data={"name": "小明", "age": 16, "address": "nanjing"}) req2 = Request.objects.create(method=1, url='/mgr/teacher/', data={"name": "小刚", "age": 18, "address": "beijing"}) req3 = Request.objects.create(method=1, url='/mgr/course/', data={"name": "小明", "age": 16, "address": "nanjing"}) def test_iquery(self): req = Request.objects.all().first() print(req) # 测试修改--整体 # res = Request.objects.filter(data__school__name="北大") # filter相当于 where子句, # 比如查找 method = 1的数据 # Request.objects.filter(method=1) # = 等号 默认精确 匹配,写sql语句的时候可能需要模糊匹配或者like等---字段条件关联查询 # 默认情况下精确匹配,大小写敏感 print('************************************') # 字段条件查询的语法是:字段__条件名 # print(Request.objects.filter(url__iexact='/MGR/course/')) # iexact:匹配模式不区分大小写的精确匹配 # print(Request.objects.filter(url__contains='course/')) # contains:包含模式 def test_in_query(self): print(Request.objects.filter(url__in=['/mgr/course/'])) # in:在..之内的匹配,后面跟一个列表,列表里包含url # 数据的url在列表里面就立刻返回,类似 BETWEEN # 选项列表表示,
其他案例: # 字段查询 class TestFieldQuery(TestCase): def setUp(self) -> None: Request.objects.create(url='/api/request/demo1',data={'name':'小 明','age':18,'addr':'nanjing','father':{'name':'xiaogang','age':40}})
def test_contains_query(self): req = Request.objects.filter(url__contains='demo') print(req)
def test_in_query(self): req = Request.objects.filter(url__in= ['/api/request/demo1','/api/request/demo2']) print(req) 执行测试:python manage.py test sqtp.tests.TestFieldQuery
7:数据库查询强化训练:跨关系查询练习
Django提供了强大并且直观的方式解决跨越关联的查询,它在后台自动执行包含JOIN的SQL语句。
要跨越某个关联,只需使用关联的模型字段名称,并使用双下划线分隔,直至你想要的字段(可以链式跨越,无限跨度)
查询步骤下面的请求:
1:先查询步骤,然后再根据步骤再反向查询请求
2:跨关系查询,不需要写中间变量,使用跨关系语法直接查询
# 查找标签是xxx的用例 res1=Case.objects.filter(tags__name='smoketest')
跨关系查询 Django提供了强大并且直观的方式解决跨越关联的查询,它在后台自动执行包含JOIN的SQL语句。
要跨越某个关联,只需使用关联的模型字段名称,并使用双下划线分隔,直至你想要的字段(可以链式跨越,无限跨度) 查找标签是xxx的用例: res1=Case.objects.filter(tags__name='smoketest')
查找 请求属于那些用例
print(req1.step.belong_case)
# 链式语法,req1 是请求,查找请求属于那个 step 步骤的,查找请求的step属性
# 再查找 step 所属的 用例case,查找 belong_case 属性 ---- 普通的 链式语法调用,对象的属性
跨关系查询:
查找请求:request
print(Request.objects.filter(step__belong_case__config__name='case001').filter(url__contains='teacher2'))
request 关系数据 如下:
step:
Request.objects.filter(step=step1)
查找 case 用例下所有的请求
Request.objects.filter(step__belong_case=self.case1) 这就是查找 case1 下面所有的请求,可以根据这种跨关系语法查找
跨关系查询:无限跨度
class Request(models.Model): """把步骤中的 Request 提取出来了,request和测试步骤 一对一的,这个request和request的http请求库别冲突了---注意""" method_choices = ( # method可选的字段, (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True) method = models.SmallIntegerField('请求方法', choices=method_choices, default=0) url = models.CharField('请求路径', default='/', max_length=1000) params = models.JSONField('url参数', null=True) headers = models.JSONField('请求头', null=True) cookies = models.JSONField('Cookies', null=True) data = models.JSONField('表单参数', null=True) json = models.JSONField('json参数', null=True) def __str__(self): return self.url
跨关系查询案例: # 跨关系查询 class TestOverRelations(TestCase): def setUp(self) -> None: # 创建用例 config1 = Config.objects.create(name='case001', base_url='http://localhost') config2 = Config.objects.create(name='case002', base_url='http://localhost') self.case1 = Case.objects.create(config=config1) self.case2 = Case.objects.create(config=config2) def test_step_request(self): # 准备测试数据 步骤和请求 step1 = Step.objects.create(belong_case=self.case1, name='step1') step2 = Step.objects.create(belong_case=self.case1, name='step2') step3 = Step.objects.create(belong_case=self.case2, name='step3') step4 = Step.objects.create(belong_case=self.case2, name='step4') req1 = Request.objects.create(method=1, url='/mgr/teacher1/', data={"name": "小刚", "age": 18, "address": "beijing"}, step=step1) req2 = Request.objects.create(method=2, url='/mgr/teacher2/', data={"name": "小刚", "age": 18, "address": "beijing"}, step=step2) req3 = Request.objects.create(method=3, url='/mgr/teacher3/', data={"name": "小刚", "age": 18, "address": "beijing"}, step=step3) req4 = Request.objects.create(method=1, url='/mgr/teacher4/', data={"name": "小刚", "age": 18, "address": "beijing"}, step=step4) print(req1.step.belong_case) # 链式语法 # 正常:根据用例查找步骤和请求 # 跨关系查询的语法: 字段__关联字段 字段和关联字段 step:数据对象 step有belong_case这个字段 # 写 step 的时候 step__belong_case 那么目标就指向了 case数据对象 # print(Request.objects.filter(step__belong_case=self.case2)) # step:数据对象,step print(Request.objects.filter(step__belong_case__config__name='case001').filter(url__contains='teacher2')) # request关联了 step,step关联了case,case关联了config,config有 name这个字段属性 # step:有belong_case 这个字段 跳到case这里 # case:有config字段 跳到 config这里 # config:有name字段 可以查询 name 名称字段 为 xxx的 如果name又是一个数据对象可以无限延展下去 # case 和 config 是一对一,假设多 对 一,通过 config去查找的话是查找 config下面所有的数据 # 跨关系数据查询:根据上级数据下级的所有数据 # 跨关系查询出来的还是一个 queryset,还可以继续调用 filter,get,first等各种条件查询 进一步精致搜索目标
案例#跨关系查询 class TestOverRelations(TestCase): def setUp(self) -> None: # 创建套件和用例 self.suite_conf = Config.objects.create(name='套件1') self.suite = Suite.objects.create(config=self.suite_conf) self.config = Config.objects.create(name='用例1') self.case = Case.objects.create(config=self.config,suite=self.suite) # 查找某套件下的用例步骤 def test_case_step(self): Step.objects.create(belong_case=self.case,name='步骤1') Step.objects.create(belong_case=self.case,name='步骤2') Step.objects.create(belong_case=self.case,name='步骤3') steps=Step.objects.filter(belong_case__suite__config__name='套件1') print(steps)
8:接口开发-DRF入门 - REST框架入门
前后端分离项目:
1:前端页面展示 vue
2:后端:数据库读取数据,操作ORM框架返回的数据处理之后返回给前端
大部分都是 json 格式的数据
Django REST framework(以下简称 DRF或REST框架)是一个开源的 Django 扩展,提供了便捷的 REST API 开发框架,拥有以下特性:
直观的 API web 界面。
多种身份认证和权限认证方式的支持。
内置了 OAuth1 和 OAuth2 的支持。
内置了限流系统。
根据 Django ORM 或者其它库自动序列化。
丰富的定制层级:函数视图、类视图、视图集合到自动生成 API,满足各种需要。
可扩展性,插件丰富。
广泛使用,文档丰富
Django REST framework:
返回 json 数据
校验接口入参
嵌套数据模型
Django第三方插件,帮助开发 出 符合 restful风格的接口
restful风格:
url定义风格
method规范:同一个数据操作测试用例,增删改查 对应不同的http请求方法
查询:get
新增:post
修改:put
删除:delete
主要操作这四种方法,主的url不变化的
对于同一个数据他的url是不变化的:这就是restful风格
使用restframework 框架:
1:安装djangorestframework和django-restfamework(已安装则忽略)
pip install djangorestframework 第三方库
2:配置djangorestframework
autotpsite/settings.py INSTALLED_APPS = [ ... 'rest_framework', 'sqtp', ]
djangorestframework 是第三方库:
安装好了之后 在 setting 配置文件里把 第三方库当成 一个 app应用程序注册到 django里,如上
3:djangorestframework运行原理
![](https://img2023.cnblogs.com/blog/2070357/202302/2070357-20230221153551231-1643465018.png)
相比于原生django开发的web应用,多了一层 序列化器(Serializer ),如果用过 Django表单(Form),应该 会对其原理有所了解,
序列化器和表单都是基于 Field 进行字段验证,而 Field 都来自于 rest_framework.fields 模块,
相当于把django的封装了一层,现在视图不直接在模型取数据,而是通过序列化工具从模型取数据
序列化工具:
序列化:
数据库里的数据需要返回给前端,不能返回 ORM 数据对象,前端不识别,需要转化成json或者xml前端才能识别
数据对象 ——> json 前端可识别的 序列表
反序列化:
前端获取来的数据,一般是 json 格式的,json格式转换成 ORM 对象才能存储到数据库,
转换的时候有各种问题,比如 数据不合法,格式不对.....,需要很多步骤来处理这些异常
使用 RF 序列化工具不需要转化,只需要告诉序列化工具具体动作,他就会自动执行
json ——> 数据对象 反序列化
序列表器:RF 做序列化和反序列化的工作的
DRF基本组件-Serializer: 序列化(Serializer)是 DRF 的核心概念,提供了数据的验证和渲染功能,其工作方式类似于 Django Form Serializer 的作用是实现序列化和反序列化 所谓序列化就是将 python 数据对象转化成 方便传输的文本格式 ,如:json/xml等 反序列化就是将这个过程反过来。和 models 类似,序列化通常定义在应用程序下单独文件中 serializer.py 如果采用了 DRF 模式开发 django,那么 model 将直接和 serializer 交互
4:使用 restframework 框架 对现有的系统做改造
1:改造模型,给模型增加一个元类
使用序列化器的时候模型里面必须定义元类信息才可以
元类:给模型提供额外的信息
1:比如模型对应的表名字
db_table = ['config'] # 如果不设置 默认名称是 app名_模型名
模型创建元类实例: from django.db import models class Config(models.Model): name = models.CharField('名称', max_length=128, unique=True) base_url = models.CharField('IP/域名', max_length=256, null=True, blank=True) # 可为空或空白 variables = models.JSONField('变量', null=True) # sqlite不支持JSONField 数据类型,需要用到mysql parameters = models.JSONField('参数', null=True) export = models.JSONField('用例返回值', null=True) verify = models.BooleanField('https校验', default=False) def __str__(self): return self.name class Meta: # 模型元类的作用,提供些额外的信息,比如模型对应的表名,每个模型增加一个元类 # db_table=['Config'] # 如果不设置 默认的名称是 app名_模型名 ordering = ['id', ] # 根据id排序,默认正序 这样写:ordering = ['-id', ] 就是倒序 class Case(models.Model): config = models.OneToOneField(Config, on_delete=models.DO_NOTHING) file_path = models.CharField('用例文件路径', max_length=1000, default='demo_case.json') def __str__(self): return self.config.name class Meta: ordering = ['id'] class Step(models.Model): # 反向查询名称 同个模型中,两个以上字段关联同一个模型,必须指定related_name # 属于那条用例 belong_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name='teststeps') # 引用的哪条用例 linked_case = models.ForeignKey(Case, on_delete=models.SET_NULL, null=True, related_name='linked_steps') name = models.CharField('名称', max_length=128) variables = models.JSONField('变量', null=True) extract = models.JSONField('请求返回值', null=True) validate = models.JSONField('校验项', null=True) setup_hooks = models.JSONField('初始化', null=True) teardown_hooks = models.JSONField('清除', null=True) def __str__(self): return self.name class Meta: ordering = ['id'] class Request(models.Model): method_choices = ( # method可选的字段, (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True) method = models.SmallIntegerField('请求方法', choices=method_choices, default=0) url = models.CharField('请求路径', default='/', max_length=1000) params = models.JSONField('url参数', null=True) headers = models.JSONField('请求头', null=True) cookies = models.JSONField('Cookies', null=True) data = models.JSONField('表单参数', null=True) json = models.JSONField('json参数', null=True) def __str__(self): return self.url class Meta: ordering = ['id']
5:创建序列化器--模型原理
视图:views.py
模型:models.py
序列化器:serializers.py 作用是转换 json 为模型对象数据,或者转行模型对象数据为 json
autotpsite/sqtp_day2/serializers.py文件 # 序列化器作用是 转化json为模型对象数据,或者转化模型对象数据为Json # 序列化器是针对数据 模型,一个 序列化器 对应一个 模型,不是一个序列化器代理系统所有的模型,一个序列化器对应一个模型 from rest_framework import serializers from sqtp.models import Step, Request # class RequestSerializer(serializers.ModelSerializer): # """创建一个针对 Request 请求模型的序列化器 # 命名规范:模型名+Serializer # 序列化器:需要继承 rest 框架 序列化器的一个基类: from rest_framework import serializers # serializers模块导入ModelSerializer类继承 # 序列化器需要针对模型的所有字段都做一个改写的 # 接口请求方面得序列化器,针对request 请求模型得序列化器 # """ # method_choices = ( # method可选的字段, # (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 # (1, 'POST'), # (2, 'PUT'), # (3, 'DELETE'), # ) # step = serializers.RelatedField(queryset=Step.objects.all(), # allow_null=True) # method = serializers.ChoiceField(choices=method_choices, default=0) # url = serializers.CharField() # params = serializers.JSONField(allow_null=True) # headers = serializers.JSONField(allow_null=True) # cookies = serializers.JSONField(allow_null=True) # data = serializers.JSONField(allow_null=True) # json = serializers.JSONField(allow_null=True) # # # 重写创建和修改方法 # def create(self, validated_data): # """ # validated_data:经过校验之后的数据, # 数据字段确定了类型,框架调用序列化创建数据时候必须经过一个校验器,校验器校验之后数据保存在validated_data里面 # 然后框架自动按调用的时候传到validated_data里面,validated_data实际上是一个字典类型 # 拿到字典类型的值直接调用相应的模型管理器的create方法将字典做一个解包动作 # """ # return Request.objects.create(**validated_data) # 将字典做解包动作,name=xxx,age=xxx这种,创建好的数据 return出去 # # def update(self, instance, validated_data): # """ # 根据提供的校验过后的数据返回一个新的实例 # instance:被修改的数据对象,实例 # validated_data:经过校验之后的入参,字典类型 # # """ # instance.step = validated_data.get("step", instance.step) # # get:返回对应key的值,没有默认返回None 参数1:key的名称 参数2:没有获取到key就返回默认值,第二个参数就是设置默认值的 # # 如果获取到值就修改,如果没有获取到值就不动,就把原来的值给他就行,不改 # # validated_data相当于前端传递来的数据,instance原本的数据对象 # instance.method = validated_data.get("method", instance.method) # instance.url = validated_data.get("url", instance.url) # instance.params = validated_data.get("params", instance.params) # instance.headers = validated_data.get("headers", instance.headers) # instance.cookies = validated_data.get("cookies", instance.cookies) # instance.data = validated_data.get("data", instance.data) # instance.json = validated_data.get("json", instance.json) class RequestSerializer(serializers.Serializer): """创建一个针对 Request 请求模型的序列化器 命名规范:模型名+Serializer 序列化器:需要继承 rest 框架 序列化器的一个基类: from rest_framework import serializers serializers模块导入ModelSerializer类继承 序列化器需要针对 模型 的所有字段都做一个 改写 的 接口请求方面得序列化器,针对request 请求模型得序列化器 """ method_choices = ( # method可选的字段, (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = serializers.RelatedField(queryset=Step.objects.all(), allow_null=True) method = serializers.ChoiceField(choices=method_choices, default=0) url = serializers.CharField() params = serializers.JSONField(allow_null=True) headers = serializers.JSONField(allow_null=True) cookies = serializers.JSONField(allow_null=True) data = serializers.JSONField(allow_null=True) json = serializers.JSONField(allow_null=True) # 重写创建和修改方法 def create(self, validated_data): """ validated_data:经过校验之后的数据, 数据字段确定了类型,框架调用序列化创建数据时候必须经过一个 校验器,校验器 校验 之后数据保存在 validated_data 里面 然后框架自动按调用的时候传到 validated_data 里面,validated_data 实际上是一个字典类型 拿到字典类型的值 直接调用相应的 模型管理器的 create 方法将字典做一个解包动作 """ return Request.objects.create(**validated_data) # 将字典做解包动作,name=xxx,age=xxx这种,创建好的数据 return出去 def update(self, instance, validated_data): """ 根据提供的校验过后的 数据 返回一个新 的实例 instance:被修改的数据对象,实例 validated_data:经过校验之后的入参,字典类型 """ instance.step = validated_data.get("step", instance.step) # get:返回对应key的值,没有默认返回None 参数1:key的名称 参数2:没有获取到key就返回默认值,第二个参数就是设置默认值的 # 如果获取到值就修改,如果没有获取到值就不动,就把原来的值给他就行,不改 # validated_data相当于前端传递来的数据,instance原本的数据对象 instance.method = validated_data.get("method", instance.method) instance.url = validated_data.get("url", instance.url) instance.params = validated_data.get("params", instance.params) instance.headers = validated_data.get("headers", instance.headers) instance.cookies = validated_data.get("cookies", instance.cookies) instance.data = validated_data.get("data", instance.data) instance.json = validated_data.get("json", instance.json) # class RequestSerializer(serializers.ModelSerializer): # """创建一个针对 Request 请求模型的序列化器 # 命名规范:模型名+Serializer # 序列化器:需要继承 rest 框架 序列化器的一个基类: from rest_framework import serializers # serializers模块导入ModelSerializer类继承 # 序列化器需要针对模型的所有字段都做一个改写的 # """ # class Meta: # model = Request # 指定序列器对应的模型 # # fields =['step','method','url','params','headers'] # 指定序列化模型中的字段 # fields = '__all__' # 序列化所有字段
models模型文件: # from django.db import models # # # # 测试平台核心模型--拆解HR用例部分 # # # class Config(models.Model): # """ # 根据 HttpRunner config 字段设计 # """ # name = models.CharField('名称', max_length=128, unique=True) # 测试用例的名称,字符串类型 # # 第一个参数 '名称' 是 verbose_name 表示这个字段,这个列,后面使用Django admin查看这个数据内容的时候 # # 如果不写这个字段显示的是name名称,这个字段是对name这个字段名字的一种显示方法,没有其他特殊含义,展示使用,列的别名 # # verbose_name='名称' 或者 放在第一个字段 这样定义 # # max_length:最大长度 # # unique=True:唯一,用例名称唯一 # # base_url = models.CharField('IP/域名', max_length=256, null=True, blank=True) # 可为空或空白 # # 主机地址 # # max_length:最大长度 # # null:可以为空 # # blank:空白,空的字符串也是可以的 # # null 和 black空白:null是一个数据类型null,空白是一个空的字符串 # # variables = models.JSONField('变量', null=True) # sqlite不支持JSONField 数据类型,需要用到mysql # # variables:里面的 值 存储的是一个字典类型 a = xxx # # null=True 允许为空,json为null和 sql的null 有区别 # # parameters = models.JSONField('参数', null=True) # # varA: [aaa, bbb] # # parameters 里面也是一个字典,字典里面存储的 键值对,键值对里的值又是一个列表 parameters做数据驱动的 # # export = models.JSONField('用例返回值', null=True) # # export里面的内容直接就是一个列表 [aaa, bbb, ccc] 导出的变量 # # 当前用例返回值,其他用例引用这个测试用例的时候可以引用 export 这里导出的变量 # # verify = models.BooleanField('https校验', default=False) # # # default=False:默认False不校验HTTPS,设置True需要指定request请求的证书 # # def __str__(self): # return self.name # # # class Case(models.Model): # """测试用例 case""" # config = models.OneToOneField(Config, on_delete=models.DO_NOTHING) # # 一对一的关联关系:指向 config # # on_delete=models.DO_NOTHING 一对一关联关系删除了,config如果被删除了那么这个case 什么都不做,保留 # # 用例可能被其他case引用 # # file_path = models.CharField('用例文件路径', max_length=1000, default='demo_case.json') # # # 用例文件路径,数据存储在数据库,需要把数据从数据库提取出来按照层级结构写到文件里,用例路径 # # 用例路径存放在case这里,方便访问 # # default:默认名称 demo_case.json 用例文件轻松转成json,转yaml需要多一个步骤 # # def __str__(self): # return self.config.name # 用例名称和所在的目录做一个组合存储到 file_path 里,这里就是外键 Config 的名称 # # config的名称就等同 用例的名称 # # # class Step(models.Model): # """定义测试步骤""" # # 反向查询名称 同个模型中,两个以上字段关联同一个模型,必须指定related_name # # 步骤属于那条用例 多对一 # belong_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name='teststeps') # # 用例被删除了,步骤就没有存在的必要,步骤也跟着删除就行 步骤无主存在没有什么必要 # # # 步骤引用的哪条用例 外键 指向 Case # linked_case = models.ForeignKey(Case, on_delete=models.SET_NULL, null=True, related_name='linked_steps') # # on_delete:引用的用例删除了,这个步骤存在还是有必要的 # # 引用的用例删除了,这个步骤的数据结构还可以存在 # # models.SET_NULL:表示引用的用例删除了,设置为null 允许为空 # # 同一个模型有两个外键字段,并且两个外键字段指向同一个模型 # # # 同一个模型有两个外键字段,并且两个外键字段指向同一个模型 :正向查询通过 belong_case 和 linked_case就行 # # 反向查询的时候 Case.step_set 这样查询获取到模型管理器 再 all等操作 返回queue——set数据类型 # # 但是 Case.step_set这样反向查询 无法反向 指定 指向 belong_case 或者 linked_case 不知道 # # 不知道反向查询是查询这条 case 下的 有哪些步骤 还是这条用例被哪些步骤所引用 无法查询出来 # # 反向查询的时候 一个数据里多条外键 多个外键指向同一个模型 这个被指向的模型 做反向查询的时候 查询不出具体指向 # # 使用:related_name 指向反向查询名称:同个模型中,两个以上字段关联同一个模型,必须指定 related_name,并且名字不能相同 # # 后面反向查询不需要 Case.step_set 这样查询 直接 Case.related_name的名字去查询 # # name = models.CharField('名称', max_length=128) # 测试步骤同名很正常 # variables = models.JSONField('变量', null=True) # extract = models.JSONField('请求返回值', null=True) # 当前http响应中提取参数,提取当前步骤响应结果的 # validate = models.JSONField('校验项', null=True) # setup_hooks = models.JSONField('初始化', null=True) # teardown_hooks = models.JSONField('清除', null=True) # # def __str__(self): # return self.name # # # class Request(models.Model): # """把步骤中的 Request 提取出来了,request和测试步骤 一对一的,这个request和request的http请求库别冲突了---注意""" # method_choices = ( # method可选的字段, # (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 # (1, 'POST'), # (2, 'PUT'), # (3, 'DELETE'), # ) # step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True) # # 关联步骤:models.CASCADE步骤删除了request也删除,请求和步骤属于扩展关系, # # request已经独立出来了:可能步骤没有请求,链接了其他用例,可以设置为 null=True # # method = models.SmallIntegerField('请求方法', choices=method_choices, default=0) # # 指定选项值:SmallIntegerField:整数类型,小范围整数节省内存空间 # # 1:先创建一个二维元组 method_choices 如上 # # 2:字段指定范围选择需要加上 choices 选项决定,指向 method_choices 二维元组 # # default=0 默认值为 get 请求 # # url = models.CharField('请求路径', default='/', max_length=1000) # # 可能请求路径就是根路径 所以 default='/' # # params = models.JSONField('url参数', null=True) # headers = models.JSONField('请求头', null=True) # cookies = models.JSONField('Cookies', null=True) # data = models.JSONField('表单参数', null=True) # json = models.JSONField('json参数', null=True) # # def __str__(self): # return self.url from django.db import models class Config(models.Model): name = models.CharField('名称', max_length=128, unique=True) base_url = models.CharField('IP/域名', max_length=256, null=True, blank=True) # 可为空或空白 variables = models.JSONField('变量', null=True) # sqlite不支持JSONField 数据类型,需要用到mysql parameters = models.JSONField('参数', null=True) export = models.JSONField('用例返回值', null=True) verify = models.BooleanField('https校验', default=False) def __str__(self): return self.name class Meta: # 模型元类的作用,提供些额外的信息,比如模型对应的表名,每个模型增加一个元类 # db_table=['Config'] # 如果不设置 默认的名称是app名_模型名 ordering = ['id', ] # 根据id排序,默认正序 这样写:ordering = ['-id', ] 就是倒序 class Case(models.Model): config = models.OneToOneField(Config, on_delete=models.DO_NOTHING) file_path = models.CharField('用例文件路径', max_length=1000, default='demo_case.json') def __str__(self): return self.config.name class Meta: ordering = ['id'] class Step(models.Model): # 反向查询名称 同个模型中,两个以上字段关联同一个模型,必须指定related_name # 属于那条用例 belong_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name='teststeps') # 引用的哪条用例 linked_case = models.ForeignKey(Case, on_delete=models.SET_NULL, null=True, related_name='linked_steps') name = models.CharField('名称', max_length=128) variables = models.JSONField('变量', null=True) extract = models.JSONField('请求返回值', null=True) validate = models.JSONField('校验项', null=True) setup_hooks = models.JSONField('初始化', null=True) teardown_hooks = models.JSONField('清除', null=True) def __str__(self): return self.name class Meta: ordering = ['id'] class Request(models.Model): method_choices = ( # method可选的字段, (0, 'GET'), # 参数1:实际存储在数据库中的值, 参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True) method = models.SmallIntegerField('请求方法', choices=method_choices, default=0) url = models.CharField('请求路径', default='/', max_length=1000) params = models.JSONField('url参数', null=True) headers = models.JSONField('请求头', null=True) cookies = models.JSONField('Cookies', null=True) data = models.JSONField('表单参数', null=True) json = models.JSONField('json参数', null=True) def __str__(self): return self.url class Meta: ordering = ['id']
test_serializers.py 测试使用序列化器文件 from django.test import TestCase # 继承Django的测试类 from sqtp.models import Step, Request from sqtp_day2.serializers import RequestSerializer # 导入序列化器 from rest_framework.renderers import JSONRenderer # 解析类:序列化的, from rest_framework.parsers import JSONParser # 解析类:反序列化的,json数据转行成对象,对象转换成json # 测试使用序列化器 class TestRequestSerializer(TestCase): # 创建一个request数据对象 req1 = Request.objects.create(method=1, url='/mgr/teacher1/', data={"name": "小刚", "age": 18, "address": "beijing"}) # 步骤一:模型数据对象转换成 python原生数据 # 序列化1:数据对象req1 转化成python原生数据类型 req1_serializer,这里已经将数据对象转换成了原生数据类型, # 原生数据类型(python的原生数据类型,比如字典) 在序列化对象 req1_serializer的 data属性里 req1_serializer = RequestSerializer(req1) print(req1_serializer.data) # 序列化后的数据存储于序列化对象的data属性中,序列化对象req1_serializer获取他的 data # {'step': None, 'method': 1, 'url': '/mgr/teacher1/', # 'params': None, 'headers': None, 'cookies': None, # 'data': {'name': '小刚', 'age': 18, 'address': 'beijing'}, 'json': None} # 这里返回的是一个字典数据类型,还不是json类型的数据,json数据没有 None,一般都是null,当前数据还是python数据类型 # 步骤二:python原生数据 转换成 json 格式,打印的是b 字节数据,字节数据类型json格式,这种数据类型可以直接互联网传输 字节码 # 当前只能看到解释器解释出来的一个字节编码,编码之后的字节形式b,没有中文,只能看到ascii 和数字,其他字符都是字节类型按照解释器显示的某种解码格式展示 # 序列化2: python原生数据类型转化为Json,json类型的值为 null json类型和字典互相转换 content = JSONRenderer().render(req1_serializer.data) print(content) # b'{"step":null,"method":1,"url":"/mgr/teacher1/","params":null,"headers":null,"cookies":null, # "data":{"name":"\xe5\xb0\x8f\xe5\x88\x9a","age":18,"address":"beijing"},"json":null}' # JSONRenderer序列化器:把 python原生数据对象转换成 json 数据类型 # 反序列化步骤一:前端传递过来的就是类似 上面 content这种字节码,网络传递的都是字节码,需要接受数据反序列化数据 # 反序列化1: 将数据流(bytes类型)解析为Python原生数据类型,也就是b字节码转换成dict字典 import io # io库处理 input output数据流的 steam = io.BytesIO(content) # 构建一个steam流 print(steam) # <_io.BytesIO object at 0x000001ACD30DE680> data = JSONParser().parse(steam) # steam流转化成python原生数据类型 # JSONParser反序列化工具,将steam流里面的json数据转行成 python 语言的数据类型 # steam流里面不是 json类型的那么JSONParser就无法 转化,只能转化json数据类型 print(data) # {'step': None, 'method': 1, 'url': '/mgr/teacher1/', # 'params': None, 'headers': None, 'cookies': None, # 'data': {'name': '小刚', 'age': 18, 'address': 'beijing'}, 'json': None} # 这里又把 数据流转化成 data 字典类型了 # 反序列化步骤二:python原生数据 data 转化成模型对象实例 # 反序列化2: serializer = RequestSerializer(data=data) # 构建序列化器 # 前面 req1_serializer = RequestSerializer(req1) 也构建了序列化器,这里知道 模型对象数据对象 req1 # 现在反序列化需要得到模型数据对象,需要原生数据 data传递到 data参数里,反序列化 if serializer.is_valid(): # 校验入参是否合法 print(serializer.validated_data) # 校验之后的数据,合法通过后把数据传递到 validated_data里面 # validated_data数据内容:OrderedDict([('step', None), ('method', 1), # ('url', '/mgr/teacher1/'), ('params', None), ('headers', None), # ('cookies', None), ('data', {'name': '小刚', 'age': 18, 'address': 'beijing'}), ('json', None)]) # validated_data是校验之后的数据,OrderedDict:类字典数据,键值对做成了元组形式,数据对象可以调用 字典的 get 方法,类字典数据 serializer.save() # 保存数据对象,校验通过之后再保存数据对象 # 序列化器返回完整结果集 serializer = RequestSerializer(Request.objects.all(), many=True) # 构建一个系列化器 print(serializer.data) # 查询结果集 # 这里把 Request.objects.all() 查询到的结果以序列化器的方式展示 # 构建序列化器的时候,这里把 Request.objects.all() 这段内容 传递给 QuerySet 里面了 # # 序列化器内部代码 print(repr(serializer))
9:序列化与反序列化
1.模型改造,添加元类 Meta
知识点:
模型的元数据,指的是“除了字段外的所有内容”,
例如 排序方式、数据库表名、人类可读的单数或者复数名等等。所有的这些都是非必须的,
甚至元数据本身对模型也是非必须的。但是,我要说但是,有些
元数据选项能给予你极大的帮助,在实际使用中具有重要的作用,是实际应用的‘必须’。
想在模型中增加元数据,方法很简单,在模型类中添加一个子类,名字是固定的 Meta ,然后在这个
Meta类下面增加各种 元数据选项或者说设置项。参考下面的例子:
from django.db import models class Request(models.Model): ... class Meta: # 模型元类的作用:为模型增加额外的信息,如模型对应表名, ordering = ['id'] # 数据根据ID排序 db_table = ['reqquest'] # 模型对应的数据库表名,不设置则默认为app名_模型名 其他可以设置的Meta选项参考附录
2.创建1个序列化类
开发我们的Web API的第一件事是为我们的Web API提供一种将 代码片段 实例序列化和反序列化为诸如
json 之类的表示形式的方式。 在 sqtp的目录下创建一个名为 serializers.py`文件,并添加以下内容。
from rest_framework import serializers from sqtp.models import Step, Request class RequestSerializer(serializers.Serializer): method_choices = ( # method可选字段,二维元组 (0, 'GET'), # 参数1:保存在数据库中的值,参数2:对外显示的值 (1, 'POST'), (2, 'PUT'), (3, 'DELETE'), ) step = serializers.RelatedField(queryset=Step.objects.all(),allow_null=True) method = serializers.ChoiceField(choices=method_choices, default=0) url = serializers.CharField() params = serializers.JSONField(allow_null=True) headers = serializers.JSONField(allow_null=True) cookies = serializers.JSONField(allow_null=True) data = serializers.JSONField(allow_null=True) json = serializers.JSONField(allow_null=True)
def create(self, validated_data): """ 根据提供的验证过的数据创建并返回一个新的`Snippet`实例。 """ return Request.objects.create(**validated_data)
def update(self, instance, validated_data): """ 根据提供的验证过的数据创建并返回一个新的`Snippet`实例。 """ instance.step =validated_data.get('step',instance.step) instance.method =validated_data.get('method',instance.method) instance.url =validated_data.get('url',instance.url) instance.params =validated_data.get('params',instance.params) instance.headers =validated_data.get('headers',instance.headers) instance.cookies =validated_data.get('cookies',instance.cookies) instance.data =validated_data.get('data',instance.data) instance.json =validated_data.get('json',instance.json)
3:使用序列化类 在我们进一步了解之前,我们先来熟悉使用我们新的Serializer类。新建测试文件sqtp/test_serializers.py, 好的,像下面一样导入几个模块,然后开始创建一些代码片段来处理
from django.test import TestCase from sqtp.models import Step, Request from sqtp.serializers import RequestSerializer from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser class TestRequestSerializer(TestCase): req1=Request.objects.create(url='/demo1',data={'name':'小明','age':18,'addr':'nanjing'}) req2=Request.objects.create(url='/demo1',params={'name':'小明','age':18,'addr':'nanjing'}) # 序列化:数据对象-->>python原生数据类型 req1_serializer = RequestSerializer(req1) print(req1_serializer.data) # 序列化数据存储在序列化对象的data属性中 【输出一个字典数据类型或者字符串数据类型】 # 序列化:python原生数据类型-->>json content = JSONRenderer().render(req1_serializer.data) # 【输出json格式的 bytes类型】 print(content)
执行测试: python manage.py test sqtp.test_serializers 返回: {'step': None, 'method': 0, 'url': '/demo1', 'params': None, 'headers': None,
'cookies': None, 'data': {'name': '小明', 'age': 18, 'addr': 'nanjing'}, 'json': None} b'{"step":null,"method":0,"url":"/demo1","params":null,"headers":null,"cookies":null,
"data":{"name":"\xe5\xb0\x8f\xe6\x98\x8e","age":18,"addr":"nanjing"},"json":null}'
反序列化是类似的。测试类再新增以下代码,实现反序列化
# 反序列化1: 将流(stream)解析为Python原生数据类型. import io stream = io.BytesIO(content) # 构建1个steam流 data = JSONParser().parse(stream) # 转成python原生数据类型 # 反序列化2: Python原生数据类型恢复成正常的对象实例。 serializer = RequestSerializer(data=data) # 序列化器 print(serializer.is_valid()) # 校验数据是否合法 print(serializer.validated_data) # 查看数据对象 serializer.save() # 保存数据
执行测试: python manage.py test sqtp.test_serializers 返回: True OrderedDict([('step', None), ('method', 0), ('url', '/demo1'), ('params', None), ('headers', None), ('cookies', N one), ('data', {'name': '小明', 'age': 18, 'addr': 'nanjing'}), ('json', None)])
除了序列化模型实例,序列化器还可以序列化查询结果集(querysets),只需要为serializer添加一个 many=True 标志。 # 序列化查询结果集(querysets) serializers= RequestSerializer(Request.objects.all(),many=True) print(serializers.data)
10:改进序列化器(掌握)
以上案例是为了大家可以了解序列化器的工作过程,在实际的项目中不会写这么繁复的代码,我们可以 用 ModelSerializer 代替 Serializer 类作为 自定义序列化器 的父类,减少重复代码的编写。 打开sqtp/serializers.py,使用 ModelSerializer 重构序列化类。 class RequestSerializer(serializers.ModelSerializer): class Meta: model = Request fields = ['step','method','url','params','headers','params','data','json'] 其实,相应的字段已经隐含在类中了,继续在测试类中添加代码,查看序列化器的内部结构 print(repr(serializer)) # 打印序列化器类实例的结构(representation)查看它的所有字段 输出: RequestSerializer(data={'id': 13, 'step': None, 'method': 0, 'url': '/demo1', 'params': None, 'headers': None, 'data': {'name': '小明', 'age': 18, 'addr': 'nanjing'}, 'json': None}): id = IntegerField(label='ID', read_only=True) step = PrimaryKeyRelatedField(allow_null=True, queryset=Step.objects.all(), required=False, validators=[<Un iqueValidator(queryset=Request.objects.all())>]) method = ChoiceField(choices=((0, 'GET'), (1, 'POST'), (2, 'PUT'), (3, 'DELETE')), label='请求方法', requir ed=False, validators=[<django.core.validators.MinValueValidator object>, <django.core.validators.MaxValueValida tor object>]) url = CharField(label='请求路径', max_length=1000, required=False) params = JSONField(allow_null=True, decoder=None, encoder=None, label='Url参数',
required=False, style={'base_template': 'textarea.html'}) headers = JSONField(allow_null=True, decoder=None, encoder=None, label='请求头', required=False, style={'ba se_template': 'textarea.html'}) data = JSONField(allow_null=True, decoder=None, encoder=None, label='Data参数', required=False, style={'bas e_template': 'textarea.html'}) json = JSONField(allow_null=True, decoder=None, encoder=None, label='Json参数', required=False, style={'bas e_template': 'textarea.html'}) ModelSerializer 类并不会做任何特别神奇的事情,它们只是创建序列化器类的快捷方式: 一组自动确定的字段。 默认简单实现的 create() 和 update() 方法
10:实际工作中使用序列号器的实例
10:DRF实践--视图编写(理解)
序列化器最终的作用是为 视图 提供转化后的数据,让我们看看如何使用我们新的 Serializer 类编写一些API视图。
目前我们不会使用任何REST框架的其他功能,我们只需将 视图 作为常规 Django 视图编写。
编辑 sqtp/views.py,导入以下库 from django.http import HttpResponse, JsonResponse from django.views.decorators.csrf import csrf_exempt from rest_framework.parsers import JSONParser from sqtp.models import Request from sqtp.serializers import RequestSerializer 编写一个视图可以返回所有的请求数据: def request_list(request): if request.method == 'GET': serializer= RequestSerializer(Request.objects.all(),many=True) return JsonResponse(serializer.data, safe=False) # safe=False是为了支持{}以外的python对象转json
创建sqtp/urls.py, 写入路由 from django.urls import path from sqtp import views urlpatterns = [ path('requests/',views.request_list) ]
总路由引入子路由 autotpsite/urls.py from django.urls import path,include urlpatterns = [ path('',include('sqtp.urls')) ]
启动server服务并测试: python manage.py runserver 浏览器访问:http://127.0.0.1:8000/requests/ :可以看到返回json格式的数据。
10:接口开发-DRF-进阶(掌握)
接口开发本质上是处理 请求 和 响应,包括了处理请求参数,判断请求方法,处理响应字段,响应码等,
本身是个枯燥的活,DRF框架为你提供自动处理这些枯燥工具的方法。先来看第一个工具,函数视图装
饰器@api_view
11:接口开发-DRF-进阶(掌握) api_view 的使用
接口开发本质上是处理请求和响应,包括了处理 请求参数,判断 请求方法,处理响应字段,响应码等,本身是个枯燥的活,
DRF框架为你提供自动处理这些枯燥工具的方法。先来看第一个工具,函数视图装饰器@api_view
from rest_framework.decorators import api_view
一般我们判断请求方法为 get 的写法: def request_list(request, format=None): if request.method=='GET': serializer = RequestSerializer(Request.objects.all(), many=True) print(request.data) return Response(serializer.data)
@api_view: 装饰器作用
1:确保视图里接收到一个 request,对request做一个封装,把request处理成Django-rest框架的一个request
2:请求对象 Request.object
请求做封装,request对象,request可以调用request.data方法
request.POST # 只处理表单数据, 只适合 POST 方法
request.data # 处理任意数据,适用于 POST,PUT,PATCH方法
3:响应对象 Response object
return Response(data) # 渲染成客户端请求的内容类型
默认情况下,Response把响应内容嵌套在默认的模板中返回出去,所以我们看到的内容是网页形式,而不是单纯的数据
调用 Response 返回的时候
调用一下模板,看到的页面是调用了 rest 框架内部的一个模板,将请求的json数据渲染到模板里面,我们看到的样子
假设做一个接口新增:需要获取到请求数据
传统获取接口请求数据方法:request.POST,request.data
使用 rest 框架之后:通过视图装饰器 @api_view 装饰以后,可以使用 request.data 去处理任何请求体提交的数据
通过 post 请求还是put还是patch等其他方法提交的数据都可以 通过 request.data 处理
通过请全体 body 传输的都可以 使用 request.data 去获取
from django.http import HttpResponse, JsonResponse from rest_framework.decorators import api_view from sqtp.models import Request from rest_framework.response import Response from sqtp.serializers import RequestSerializer @api_view(['GET']) def request_list(request): if request.method == 'GET': serializer = RequestSerializer(Request.objects.all(), many=True) return Response(serializer.data) # safe=False是为了支持{}以外的python对象转json
浏览器访问:http://127.0.0.1:8000/requests/
哇塞,和单纯的显示数据相比,有网页内容了
该装饰器的作用是 确保你在 视图 中接收到 Request 实例,并将上下文添加到 Response ,以便可以执行 内容协商
包装器还提供了诸如在适当时候返回 405 Method Not Allowed 响应,并处理在使用格式错误的输入来 访问 request.data 时发生的任何 ParseError 异常
11:知识点:请求与响应
在纯django中,request 和response 只能实现有限的功能。REST框架对其作了扩展,其中:
请求对象(Request objects): REST框架引入了一个扩展了常规 HttpRequest 的 Request 对象,并提供了更灵活的请求解析。
Request 对象的核心功能是 request.data 属性,它与 request.POST 类似,但对于使用Web API更为有用
request.POST # 只处理表单数据 只适用于'POST'方法 request.data # 处理任意数据 适用于'POST','PUT'和'PATCH'方法
响应对象(Response objects): REST框架还引入了一个 Response 对象,这是一种获取未渲染(unrendered)内容的TemplateResponse 类型,
并使用内容协商来确定返回给客户端的正确内容类型。 return Response(data) # 渲染成客户端请求的内容类型。 默认情况下,Response 把响应内容嵌套在默认的模板中返回出去,所以我们看到的内容是网页形式,而不是单纯的数据
扩展-响应数据格式: 如果我们要开发前后端分离的系统,又不能返回这种 html 格式的,需要json格式的该怎么处理呢? REST框架充分考虑道了这一点,为了充分利用我们的 响应 不再与 单一内容类型连接,
我们可以为API 路径添加对 格式后缀的支持。使用 格式后缀 给我们明确指定了给定格式的 URL,这意味着我们的API将能够 处理诸如http://example.com/api/items/4.json之类的URL。 视图中添加一个 format 关键字参数。 @api_view(['GET']) def request_list(request,format=None): if request.method == 'GET': serializer = RequestSerializer(Request.objects.all(), many=True) return Response(serializer.data) # safe=False是为了支持{}以外的python对象转json 修改路由文件 sqtp/urls.py from django.urls import path from sqtp import views from rest_framework.urlpatterns import format_suffix_patterns urlpatterns = [ path('requests/',views.request_list) ] urlpatterns = format_suffix_patterns(urlpatterns) 重启服务器,访问http://127.0.0.1:8000/requests/ ,默认情况下还是网页 访问http://127.0.0.1:8000/requests.json ,返回了json格式的数据,完美!
11:扩展-查询单个数据
前面我们开发的接口是批量列出,如果要开发列出单个数据,需要开发另外接口 @api_view(['GET']) def request_detail(request,_id,format=None): """ 获取,更新或删除一个req实例。 """ try: req_obj = Request.objects.get(pk=_id) except Request.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) # status提供常用http状态码 if request.method == 'GET': serializer = RequestSerializer(req_obj) return Response(serializer.data)
更新路由:sqtp/urls.py urlpatterns = [ path('requests/', views.request_list), path('requests/<int:_id>', views.request_detail) ] urlpatterns = format_suffix_patterns(urlpatterns)
访问http://127.0.0.1:8000/requests/2 或 http://127.0.0.1:8000/requests/2.json 查看对应数据 其中数字代表对应数据的ID
11:可用的Meta选项:模型 Meta 选项 | Django 文档 | Django (djangoproject.com) https://docs.djangoproject.com/zh-hans/3.2/ref/models/options/
12:视图函数增删改查编写
# 老版本view视图函数编写:request请求 案例 from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response from sqtp.models import Request from sqtp.serializers import RequestSerializer @api_view(['GET', 'POST']) # 列表中是允许的请求方法 def request_list(request, format=None): if request.method == "GET": # GET 方法处理查询请求 serializer = RequestSerializer(Request.objects.all(), many=True) print(Request.objects.all()) return Response(serializer.data) elif request.method == "POST": # POST 就处理新增请求 # 处理新增数据:接口传过来json(序列化数据) ——> 将json转成数据化对象data-obj(反序列化数据) 反序列化过程 # request.data 这个就能拿到 python 原生数据对象,不是json流,是python原生数据对象 # 这里 request 是 rest框架 包装过的,拿到 request 可以直接.data拿到请求体里的数据,自动转换成 json原生数据 了 # 反序列化过程:前端传递的数据 使用序列化器 校验,然后保存 3步骤 serializer = RequestSerializer(data=request.data) # 1:构建序列化器,把request.data传递进去 # 指定data传参,默认第一个参数是instance, if serializer.is_valid(): # 2:校判断入参是否合法 print(serializer.validated_data) serializer.save() # 3:合法那么就保存数据对象,校验通过之后再保存数据对象 return Response(serializer.data, status=status.HTTP_201_CREATED) # 把保存的数据返回出去 else: # 如果数据不合法,返回错误信息,错误信息传到了errors这里:什么数据不对,不符合要求 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # 客户端请求的数据不合法应该告诉前端 4xx @api_view(['GET', 'PUT', 'DELETE']) def request_detail(request, _id, format=None): try: req_obj = Request.objects.get(id=_id) except Exception as e: print(e) return Response(status=status.HTTP_404_NOT_FOUND) # 如果传进来的 id 查询不到,get就会报错 if request.method == "GET": # get请求的话就是查询单个数据,把拿到的数据 return出去 serializer = RequestSerializer(req_obj) # 序列化,查询需要单独的序列号器,和修改需要的序列号器不同 # 这个序列化器:把数据库查询到的数据做一个反序列化 return Response(serializer.data) elif request.method == "PUT": # 处理修改 # 修改:采用序列号器实现 修改动作 # 后面常规的增删改查不操作Django的ORM了,操作request提供的序列化器 serializer = RequestSerializer(req_obj, data=request.data) # 这个序列化器:不仅仅需要反序列化,还需要把待修改的数据放到data里面 # serializer = RequestSerializer(data=request.data) 新增构建的序列化器第一个参数 req_obj = Request.objects.get(id=_id) 没有传递 # 没有传就会新增一条数据 # 当前 serializer = RequestSerializer(req_obj, data=request.data) 传了数据,req_obj # 当前 data 数据会把 req_obj 数据覆盖掉,这就是新增和 修改的区别,序列号器需要分别构建 if serializer.is_valid(): # 判断 data中的数据是否合法,符合要求,是否符合模型当中要求,比如name要求一个字符串,当前传递一个数字name就不符合要求 serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) # 默认返回就是200 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # 如果数据不合法,返回400,并且把异常信息返回 elif request.method == "DELETE": # 处理删除 req_obj.delete() # 调用模型的 delete 方法就行,删除不需要使用序列化器,拿到 模型对象调用delete return Response({"code": 200, "message": "success"}, status=status.HTTP_204_NO_CONTENT) """ 接口规范: get:/requests/<id> post:/requests/ put:/requests/<id> delete:/requests/<id> rest风格:同一数据主体部分 url是不变化的 rest风格主体的风格不变化,后面带 id是为了操作具体数据所以传递了参数 request做一个新增,原来基础上完成接口,url不变化,不要再创建一个 add_requests rest风格的接口的url里没有动词,只有名称----rest 风格 在 requests 这个 url路由上实现 post等各种请求 path('requests/', views.request_list), request指向 request_list 这个视图函数 request_list: 列出功能,还可以在一个视图实现add新增,delete删除等各种功能 使用一个视图函数完成多件事件 使用 if 判断 """
13:REST 框架类视图
1:ApiView
14:资源的增删改查【05】
新增资源: 继续更新视图,sqtp/views.py ,编辑 request_list 视图,增加添加方法
@api_view(['GET','POST']) # 允许的请求方法 def request_list(request,format=None): if request.method == 'GET': # 构造序列化器 serializer = RequestSerializer(Request.objects.all(),many=True) # 返回json格式数据 return Response(serializer.data) # 将python原生格式转成json数据 safe=False是为了支持{}以外的python对象转json elif request.method == 'POST': serializer = RequestSerializer(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) 刷新浏览器,测试新增方法,输入以下内容后,点POST: {"step":null,"method":0,"url":"/demo1","params":null,"headers":null,"cookies":null,
"json":{"name":"方方","age":18,"addr":"nanjing"},"data":null} 成功返回: HTTP 201 Created Allow: POST, GET, OPTIONS Content-Type: application/json Vary: Accept { "step": null, "method": 0, "url": "/demo1", "params": null, "headers": null, "data": null, "json": { "name": "方方", "age": 18, "addr": "nanjing" } }
修改资源: 修改资源往往是针对1个数据的,所以接下来修改 requset_detail 视图。修改使用的是 PUT 方法,增加装饰器可用的方法和请求方法判断 @api_view(['GET','PUT']) def requset_detail(request,_id,format=None): try: req_obj = Request.objects.get(pk=_id) # 根据id查找单个数据 except Exception: return Response(status=status.HTTP_404_NOT_FOUND) if request.method=='GET': # 获取单个数据的方法 serializer = RequestSerializer(req_obj) return Response(serializer.data) # 返回单个数据 elif request.method == 'PUT': # 修改使用PUT方法 serializer = RequestSerializer(req_obj,data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) # 如果数据不合法就返回400 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 测试修改方法,访问单个资源页面http://127.0.0.1:8000/requests/1,输入以下数据: { "step": null, "method": 0, "url": "/example/demo2", "params": { "age": 66, "addr": "nanjing", "name": "哈哈哈" }, "headers": {"content_type":"json"}, "data": {}, "json": null } 成功返回修改后的数据内容: HTTP 200 OK Allow: OPTIONS, GET, PUT Content-Type: application/json Vary: Accept { "step": null, "method": 0, "url": "/example/demo2", "params": { "age": 66, "addr": "nanjing", "name": "哈哈哈" }, "headers": { "content_type": "json" }, "data": {}, "json": null }
删除资源: 与修改一样,删除也是针对 1 个数据的,接下来修改 requset_detail 视图,增加 删除 部分,同样,删除采用的是DELETE方法,所以增加准入方法和DELETE判断。 # 返回单个数据 @api_view(['GET','PUT','DELETE']) def requset_detail(request,_id,format=None): try: req_obj = Request.objects.get(pk=_id) # 根据id查找单个数据 except Exception: return Response(status=status.HTTP_404_NOT_FOUND) if request.method=='GET': serializer = RequestSerializer(req_obj) return Response(serializer.data) elif request.method == 'PUT': # 修改使用PUT方法 serializer = RequestSerializer(req_obj,data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) # 如果数据不合法就返回400 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': req_obj.delete() # 直接调用数据对象的delete方法 return Response(status=status.HTTP_204_NO_CONTENT) 测试删除方法,访问单个资源页面http://127.0.0.1:8000/requests/1,发现多了DELETE按钮,点击即可完成删除
15:基于类的视图 【05】
作为开发要不断思考如何让代码保持高内聚,低耦合,因此优化代码的道路上一直都不停歇。
目前我们开发的视图是基于函数形式的,特点是灵活,缺点是功能冗余性大,面对常见的 增删改查 往往要写重复的代码。 REST框架可以用基于类的视图来优化代码结构,我们来一探究竟:用类重写视图,继承REST框架的APIView类 from rest_framework.views import APIView class RequestList(APIView): """ 列出所有的requests或者创建一个新的request. """ def get(self,request,format=None): # 构造序列化器 serializer = RequestSerializer(Request.objects.all(),many=True) # 返回json格式数据 return Response(serializer.data) # 将python原生格式转成json数据 safe=False是为了支持{}以外的python对象 def post(self,request): serializer = RequestSerializer(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)
以上代码完成了 查询列表 和 新增单个数据的功能,我们发现除了变成了类的形式,新增了get(处理get 请求)和post(处理post请求)方法,其他的几乎都没变
同时,也更改下详情视图,用类视图重构: class RequestDetail(APIView): # 覆盖父类的get_object方法 def get_object(self,_id): try: return Request.objects.get(pk=_id) # 根据id查找单个数据 except Exception: return Response(status=status.HTTP_404_NOT_FOUND) def get(self,request,_id,format=None): req_obj = Request.objects.get(pk=_id) serializer = RequestSerializer(req_obj) return Response(serializer.data) def put(self,request,_id,format=None): req_obj = Request.objects.get(pk=_id) serializer = RequestSerializer(req_obj,data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) # 如果数据不合法就返回400 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self,request,_id,format=None): req_obj = Request.objects.get(pk=_id) req_obj.delete() # 直接调用数据对象的delete方法 return Response(status=status.HTTP_204_NO_CONTENT)
启动服务之前,修改下sqtp/urls.py,此时我们使用的是基于类的视图 urlpatterns = [ path('requests/', views.RequestList.as_view()), # 需要调用类视图的as_view方法 path('requests/<int:_id>', views.RequestDetail.as_view()) ] urlpatterns = format_suffix_patterns(urlpatterns) 测试一下增删改查,OK。
16:基于类的视图代码优化1 【05】
使用类视图的一个好处就是可以复用相同的功能,只需传入指定的参数即可。在上面的案例中,增删改查的逻辑行为都是确定的。
REST框架为我们封装好了逻辑,我们可以用更少的代码来封装视图,比如采用 generics 模块的通用视图
class RequestList(generics.ListCreateAPIView): """ 列出所有的requests或者创建一个新的request. """ queryset = Request.objects.all() serializer_class = RequestSerializer
class RequestDetail(generics.RetrieveUpdateDestroyAPIView): queryset = Request.objects.all() serializer_class = RequestSerializer
修改路由: path('requests/<int:pk>',views.RequsetDetail.as_view()) 重启服务,测试增删改查,依然OK!
并且我们发现,数据详情的编辑页面改成了 按字段进行编辑了,这是通用类视图提供的效果。 上例中用到的两个通用类视图从名字可以知道其功能
ListCreateAPIView:提供列出所有和创建数据功能 RetrieveUpdateDestroyAPIView:提供查询、修改和删除数据功能
17:基于类的视图代码优化2 【05】
我们发现,优化后的视图代码依然存在重复的部分,那么这个部分能不能继续优化呢,答案是可以的,
我们用ViewSet(视图集)代替View类重构视图。
# 删除其余的视图函数 from rest_framework import viewsets class RequestViewSet(viewsets.ModelViewSet): queryset = Request.objects.all() serializer_class = RequestSerializer
使用 REST ViewSets 的抽象后,开发人员可以集中精力对 API 的状态和交互进行建模,并根据常规约定自动处理URL构造
ViewSet 类与 View 类几乎相同,不同之处在于它们提供诸如 read 或 update 之类的操作,而不是 get 或 put 等方法处理程序
一个 ViewSet 类只绑定到 一组方法 处理程序,当它 被实例化成一组 视图的时候,通常通过使用一个 Router 类来代替自己定义复杂的URL
路由设计的自动化:
因为我们使用的是 ViewSet 类而不是 View 类,所以连常规的URL设计我们都可以偷懒了。利用rest框架的router,可以帮助我们自动生成路由列表
重构sqtp/urls.py from rest_framework.routers import DefaultRouter # 创建路由器并注册我们的视图。 router = DefaultRouter() router.register(r'requests', views.RequestViewSet) urlpatterns = [ path('',include(router.urls)), ] router的作用是根据你注册的路由前缀,帮助你自动生成诸如此类的路由列表: ^requests/$ [name='request-list'] ^requests\.(?P<format>[a-z0-9]+)/?$ [name='request-list'] ^requests/(?P<pk>[^/.]+)/$ [name='request-detail'] ^requests/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$ [name='request-detail']
18:自定义接口规范 【05】
目前我们使用的是REST框架默认的返回格式,类似这种: [ { "id": 1, "method": 0, "url": "/demo1", "params": null, "headers": null, "data": { "age": 18, "addr": "nanjing", "name": "小明" }, "json": null }, ... ]
这样并不利于前端的渲染,我们希望接口在正确的时候返回:{"msg":"success","retcode":200,"retlist":[...]} 错误的时候返回: {"msg":"error","retcode":404,"error":error_msg} 想要弄成类似这样的效果需要自定义 drf 异常返回 和 自定义数据返回格式,REST框架为我们提供了该技术
1:在settings.py中添加drf全局配置: # rest框架配置 REST_FRAMEWORK = { # 默认的渲染器 'DEFAULT_RENDERER_CLASSES': ( # 'rest_framework.renderers.JSONRenderer', # 'rest_framework.renderers.BrowsableAPIRenderer', 'utils.renderers.MyRenderer', ) }
utils.renderers.MyRenderer对应的是你文件路径,路径起点为项目根目录,因此你需要创建utils目录 然后在下面新建renderers.py文件
renderers.py自定义渲染器内容:
from rest_framework.renderers import JSONRenderer # 继承空返回JSON的渲染器 class MyRenderer(JSONRenderer): # 重构 render方法 def render(self, data, accepted_media_type=None, renderer_context=None): print(renderer_context['response']) status_code = renderer_context['response'].status_code #响应状态码 if str(status_code).startswith('2'): # 以2开头表示响应正常 res = {'msg':'success','retcode':status_code,'retlist':data} # 返回父类方法 return super().render(res,accepted_media_type,renderer_context) else: # 异常请情况 res = {'msg':'error','retcode':status_code} return super().render(res,accepted_media_type,renderer_context)
19:渲染器基本原理 【06】
REST渲染器基本原理
序列化在返回数据后并不是直接做为 响应数据,而是经过 渲染器 的渲染,生成不同格式的 响应内容,如 html或json格式。
REST本身支持 HTML 和 json 格式,有内置的渲染器。那么如果我们想要返回 定制化的 内容就要重写渲染器,按照我们指定的格式进行响应
重构渲染器就是重写父类渲染器的render方法:
render(self, data, accepted_media_type=None, renderer_context=None)
传递给 .render() 方法的参数是:
data:响应数据(序列化器的.data属性),等同于renderer_context["response"].data的值
media_type=None 可选的。如果提供,这是由内容协商阶段确定的所接受的媒体类型。
根据客户端的 Accept: 头,这可能比渲染器的 media_type 属性更具体,可能包括媒体类型参数。
例 如 "application/json; nested=true" 。
renderer_context=None 可选的。如果提供,这是一个由view提供的上下文信息的字典。
默认情况下这个字典会包括以下键: view , request , response , args , kwargs 。
renderer_context[" view "] 对应调用的视图函数
renderer_context[" request "] 对应本次请求对象,包含所有请求数据,如请求头,请求参数等等
renderer_context[" response "] 对应本次响应对象,包含所有响应数据,如响应头,状态码,响应数据 等等
20:异常信息获取 【06】
当前情况下我们智能获取到正常情况下返回的数据,如果想返回异常信息,需要了解下REST异常处理机制
REST默认情况可以处理的异常有:
在REST framework内部产生的 APIException 的子类异常。
原生Django的 Http404 异常.
原生Django的 PermissionDenied 异常
如果我们想要获取异常数据,需要从异常处理器中提取,REST默认的是exception_handler,
当触发异常时,可以获取到异常的响应信息,我们可以自由定义其格式
使用方法是:
1.配置异常处理模块
# rest框架配置 REST_FRAMEWORK = { # 全局配置异常模块 'EXCEPTION_HANDLER': 'utils.exception.my_exception_handler', # 默认的渲染器 'DEFAULT_RENDERER_CLASSES': ( 'utils.renderers.MyRenderer', ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' }
2.自定义异常处理
增加异常处理: # utils/exceptions.py from rest_framework.views import exception_handler, Response def my_exception_handler(exc, context): # 首先调用REST framework默认的异常处理, # 以获得标准的错误响应。 response = exception_handler(exc, context) if response: # 成功捕获到异常的情况 response.data['msg'] = 'error' # 标记 response.data['retcode'] = response.status_code # 状态码 response.data['error'] = str(exc) # 详细错误原因 response.data.pop('detail') # detail的异常信息比较简单 return response 更新渲染器逻辑: # 通用返回过滤器 from rest_framework.renderers import JSONRenderer # 继承空返回JSON的渲染器 class MyRenderer(JSONRenderer): # 重构 render方法 def render(self, data, accepted_media_type=None, renderer_context=None): # 默认把data作为响应数据 resp_content = data if renderer_context: status_code = renderer_context['response'].status_code #响应状态码 if str(status_code).startswith('2'): # 以2开头表示响应正常 # 判断响应内容是否是列表形式,如果不是列表形式,则变成列表形式以对应接口要求 if not isinstance(resp_content,list): resp_content = [resp_content] res = {'msg':'success','retcode':status_code,'retlist':resp_content} # 返回父类方法 return super().render(res,accepted_media_type,renderer_context) return super().render(resp_content,accepted_media_type,renderer_context)
21:swagger在线接口文档 【06】
目前为止,我们的接口开发到了一定的阶段,已经初具规模,在和前端对接之前,需要规范化我们的接口文档。
如果纯手写的话工作量大且重复枯燥,因此,我们可以用工具帮助我们实现接口文档的自动生成。
Django REST Swagger 项目已经不维护了,并且不支持最新的Django,
所以我们选择 drf-yasg 项目作为接口文档生成器。yasg的功能非常强大,可以同时支持多种文档格式。
快速开始:
1.安装 pip install -U drf-yasg # 安装最新版 drf-yasg
2.注册:老样子,这也属于django的插件,因此需要注册到配置文件中 settings.py INSTALLED_APPS = [ ... 'django.contrib.staticfiles', # required for serving swagger ui's css/js files 'drf_yasg', ... ]
3.配置路由 from drf_yasg.views import get_schema_view from drf_yasg import openapi schema_view = get_schema_view( openapi.Info( title="SQTP API", default_version='v1', description="SQTP接口文档", terms_of_service="https://www.songqin.net", contact=openapi.Contact(email="haiwen@sqtest.org"), license=openapi.License(name="BSD License"), ), public=True, permission_classes=(permissions.AllowAny,), ) urlpatterns = [ ... path('swagger/',schema_view.with_ui('swagger',cache_timeout=0,),name='schema-swagger-ui'), # 互动模式 path('redoc/',schema_view.with_ui('redoc',cache_timeout=0),name='schema-redoc'), # 文档模式 ... ]
4.访问互动 访问http://127.0.0.1:8000/swagger/ 进入互动模式,该模式下可以对接口发起请求 访问http://127.0.0.1:8000/redoc/ 进入文档模式,该模式为静态模式,展示详细的接口内容
21:定制化用法(viewset模式) 【06】
我们当前的接口文档是没有定制化注释的,比如某个接口的功能是什么,虽然现在都是增删改查,
从名称根据Rest风格就能猜测出来,但如果是些定制化的接口就需要加些注释了
函数视图,采用swagger_auto_schema装饰器修饰视图函数: from drf_yasg.utils import swagger_auto_schema
@swagger_auto_schema(method='GET',operation_summary='定制化API',operation_description='接口描述。。') @api_view(['GET']) def customer_api(request): return Response(data={"retcode":status.HTTP_200_OK,'msg':'building...'})
如果我们的视图采用类或者视图集,视图 函数本身是继承父类,没有出现在我们的代码中该如何自定义接口描述呢? 这时,我们可以采用 django 装饰器配合 swagger 的装饰器来实现,直接装饰类视图 from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema
@method_decorator(name='list', decorator=swagger_auto_schema( operation_description="列出所有步骤数据")) @method_decorator(name='create', decorator=swagger_auto_schema( operation_description="创建步骤")) @method_decorator(name='update', decorator=swagger_auto_schema( operation_description="更新步骤")) @method_decorator(name='destroy', decorator=swagger_auto_schema( operation_description="删除步骤")) @method_decorator(name='retrieve', decorator=swagger_auto_schema( operation_description="提取单个步骤数据" class StepViewSet(viewsets.ModelViewSet): queryset = Step.objects.all() serializer_class = StepSerializer
就是这么丧心病狂的叠加装饰器,一个装饰器修饰一个方法,如果多个就多层叠加...
22:前端对接 【06】
现在接口文档整活了,对应的核心接口也开发的差不多了,接下来我们希望看到自己的实际项目前端页面
因为我们的项目是前后端分离的,我们后端开发不涉及前端页面开发,前端开发人员按照API文档去访问 后端数据,然后渲染页面。
前端开发好对应的功能后,和后端进行配合就可以看到效果了。 前端环境其实就是 一些前端的代码和资源文件,包括 js文件、html文件、css文件 还有 图片视频文件 等。
模拟前端团队开发的 前端 系统可以在百度云盘当天的课程里找到。 下载好dist压缩包后,可以解压到项目根目录下面,这个目录下面就是前端的代码资源文件。
Django的开发环境也可以从浏览器访问这些前端的资源文件。 但是前端文件都是静态文件,需要我们配置一下Django的配置文件,
指定http请求如果访问静态文件, Django在哪个目录下查找。
注意,接下来我们配置 Django 静态文件服务, 是 开发时 使用的 一种 临时方案 ,性能很低,这是方便 我们调试程序用的。
前面讲过,正式部署web服务的时候,不应该这样干,应该采用其它方法,比如Nginx等。后面的教程 会有详细的讲解如何使用Nginx 和 Django 组合使用。
打开 autotpsite/urls.py 文件,在末尾 添加一个 + static("/", document_root="dist") 并添加如下声明: # 静态文件服务 from django.conf.urls.static import static 最终,内容如下: from django.conf.urls.static import static from django.contrib import admin from django.urls import path,include urlpatterns = [ path('admin/', admin.site.urls), path('',include('sqtp.urls')) ] + static("/", document_root="dist") 最后的:+ static("/", document_root="dist")
就是在url 路由中加入 前端静态文件的查找路径。
这样如果 http请求的url 不是以 api/project 开头, Django 就会认为是要访问 dist目录下面的静态文 件。
好了,现在我们 运行如下命令,启动Django 开发服务器: python manage.py runserver 8081
当我们访问html页面时,js代码会自动请求后台数据,渲染到当前页面上,为了适配前端,减少改动工作,先将后台URL进行改动
urlpatterns = [
path('api/',include(router.urls)),
# 以api开头访问 ...
]
23:接口开发完善 【06】
为了提供更多的信息给前端,需要对目前的接口进行开发完善。以用例为例,当前的查询接口返回的消息是这种格式:
{"msg":"success","retcode":200,"retlist": [{"id":1,"file_path":"haiwen_Test.yml","config":1,"suite":null}]}
其中 config 只有id表示,没有具体的信息,如果前端想要获取config信息将会比较麻烦,所以直接通过后 端提供相关数据效果比较好点
由于我们使用了REST框架,这个问题交给序列化器就可以处理了。目前我们默认的序列化器很简单,只 简单的定义了要展示的字段和对应模型。
那么,默认情况下序列化器只会提取外键字段的 id 作为默认 值,所以我们需要额外定义需要展示 嵌套字段的关联数据。
这个问题处理起来也非常简单,我们只需指定 config 为对应的 序列化器 对象即可 # 配置 class ConfigSerializer(serializers.ModelSerializer): class Meta: model = Config fields = '__all__' # 用例 class CaseSerializer(serializers.ModelSerializer): config = ConfigSerializer() # config字段为Config序列化器,REST会自动提取其值 class Meta: model = Case fields = '__all__'
注意两个类的顺序,由于python引用前需要先定义,所以 ConfigSerializer 要在上面
再来看下最新的返回,已经展示了config嵌套字段:
{"msg": "success", "retcode": 200, "retlist": [{"id": 1,"config": {"id": 1,"name": "用例1",
"base_url": "http://localhost","variables": null,"parameters": null,"verify": false,"export": null},
"file_path": "haiwen_Test.yml","suite": null} ] }
displayname: 第二个案例我们来看下,请求数据的展示效果 { "msg": "success", "retcode": 200, "retlist": [ { "id": 1, "method": 0, "url": "/demo1", "params": null, "headers": null, "cookies": null, "data": { "age": 18, "addr": "nanjing", "name": "小明" }, "json": null, "step": 1 } ] }
method这里显示的不是GET/POST/PUT/DELETE,而是0,1,2,3 这种实际存储在数据中的值,
因为我们定义了choice选择字段,但是并没有显示我们要的注释。所以这里需要修改成显示成对应的 可 读内容
解决这个问题之前,我们可以看下 django 原生 ORM 是如何显示 choice 字段的可读内容的
python manage.py shell 先进入django shel
>>> from sqtp.models import Request
>>> req1 = Request.objects.all().first()
>>> req1.method # 默认情况下还是实际值 1
>>> req1.get_method_display() # 采用 get_field_display 方法,field对应choice字段的 名称 'POST'
这个时候我们再进入序列化器,修改字段的获取方式:
# 请求模型的序列化器 class RequestSerializer(serializers.ModelSerializer): method = serializers.SerializerMethodField() # 自定义字段序列化返回方法 def get_method(self, obj): # rest框架获取 method 时,自动调用该方法 return obj.get_method_display() # 返回choice的 displayname而不是实际值 class Meta: model = Request # 指定对应的模型 fields = '__all__' # 显示对应模型的所有字段 # 定义可以显示和操作的字段
此时重新访问接口,method字段返回了可读内容:
{ "msg": "success", "retcode": 200, "retlist": [ { "id": 1, "method": "GET", "url": "/demo1", "params": null, "headers": null, "cookies": null, "data": { "age": 18, "addr": "nanjing", "name": "小明" }, "json": null, "step": 1 } ] }
24:项目管理需要项目,模块,和测试环境等相关数据,同时原有模型需要 增加一些额外的通用字段,如增加时间,更新时间等 【07】
模型关联关系:首先确立模型的关联关系
用例和项目之间的关联可以直接通过config,因为config是套件和用例的扩展,相当于用例和套件本身
25:模块的包管理 【07】
在我们使用 python manage.py startapp xxx 命令创建新的应用时,Django会自动帮我们建立一个应用的基本文件组织结构,
其中就包括一个 models.py 文件。通常,我们把当前应用的模型都编写在这个文件里,但是如果你的模型很多,
那么将单独的 models.py 文件分割成一些独立的文件是个更好的做法
首先,我们需要在应用中新建一个叫做 models 的包,再在包下创建一个 __init__.py 文件,这样才能 确立包的身份。
然后将 models.py 文件中的模型分割到一些 .py 文件中,比如 plan.py 和 case.py ,
然后删除 models.py 文件。最后在 __init__.py 文件中导入所有的模型。如下例所示:
# sqtp/models/__init__.py from .hr3 import Config,Suite,Case,Step,Request
要显式明确地导入每一个模型,而不要使用 from .models import * 的方式,这样不会混淆命名空 间,让代码更可读,更容易被分析工具使用
最终的模型结构:
其中 hr3.py 存放 所有核心数据 模型
mgr.py 存放所有 项目管理 相关模型
mgr.py中我们暂时定义项目和环境数据模型
""" @date: 2021/6/16 @file: mgr.py """ from django.db import models class Project(models.Model): proj_status = ( ('developing', '开发中'), ('operating', '维护中'), ('stable', '稳定运行') ) # 名称 name = models.CharField(max_length=32, unique=True, verbose_name='项目名称') # 状态 status = models.CharField(choices=proj_status, max_length=32, default='stable', verbose_name='项目状态') # 版本 version = models.CharField(max_length=32, default='v1.0', verbose_name='版本') class Meta: verbose_name = '项目表' class Environment(models.Model): # 服务器类型选项 service_type = ( (0, 'web服务器'), (1, '数据库服务器'), ) # 服务器操作系统选项 service_os = ( (0, 'window'), (1, 'linux'), ) # 服务器状态选项 service_status = ( (0, 'active'), (1, 'disable'), ) project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目') # ip--使用djangoORM提供的一个叫GenericIPAddressField专门储存IP类型的字段 ip = models.GenericIPAddressField(default='127.0.0.1', verbose_name='ip地址') port = models.SmallIntegerField(default=80, verbose_name='端口号') # 服务器类型 category = models.SmallIntegerField(default=0, choices=service_type, verbose_name='服务器类型') # 操作系统 os = models.SmallIntegerField(default=0, choices=service_os, verbose_name='服务器操作系统') # 状态 status = models.SmallIntegerField(default=0, choices=service_status, verbose_name='服务器状态') class Meta: verbose_name = '测试环境表'
26:模型的继承 abstract = True # 定义为抽象表,不会创建数据库表 【07】
一般在实际项目中,数据模型除了业务字段以外,还需要有一些通用字段,如创建时间,更新时间,创建者,更新者等,
这些字段如果在每个模型都定义的话,冗余度很高,而且维护起来不方便。 此时,我们可以用一个抽象模型类来存放这些字段,然后其他模型继承该抽象模型类即可。 新建 models/base.py,定义一个抽象模型CommonInfo 如下
from django.db import models # 公共抽象模型 class CommonInfo(models.Model): # 创建 create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') # auto_now_add 第1次创建数据时自动添加当前时间 # 更新 update_time = models.DateTimeField(auto_now=True, verbose_name='创建时间', null=True) # auto_now 每次更新数据时自动添加当前时间 # 描述--文本 desc = models.TextField(null=True, blank=True, verbose_name='描述') def __str__(self): # 检查当前对象有没有name属性 # 有name就返回name,没有就返回desc if hasattr(self, 'name'): return self.name return self.desc class Meta: abstract = True # 定义为抽象表,不会创建数据库表 # 默认使用id排序 ordering = ['id']
由于在元类中定义了 abstract = True ,抽象模型在同步 数据库的时候并不会创建表,子类只会继承其字 段和方法
另外元类除了 abstract = True 不会继承,其他都会继承,若想把子类也设置为抽象模型,必须显示在元 类中设置 abstract = True
子类继承抽象父类: # mgr.py class Project(CommonInfo): ... class Environment(CommonInfo): ...
# hr3.py from django.db import models # Create your models here. from .mgr import Project from .base import CommonInfo class Config(CommonInfo): ... class Step(CommonInfo): ... # 请求 class Request(CommonInfo): ... class Case(CommonInfo): ... class Suite(CommonInfo): ...
包文件增加导入: # sqtp/models/__init__.py from .hr3 import Config,Suite,Case,Step,Request from .mgr import Project,Environment
此时同步数据库:
python manage.py makemigrations
python manage.py migrate
27:抽象基类的Meta数据: 【07】
如果子类没有声明自己的 Meta类,那么它将自动继承抽象基类的 Meta 类。
如果子类要设置自己的 Meta 属性,则需要扩展基类的 Meta:
from django.db import models class CommonInfo(models.Model): # ... 抽象基类 class Meta: abstract = True ordering = ['name'] class Student(CommonInfo): # ... class Meta(CommonInfo.Meta): # 注意这里有个继承关系 db_table = 'student_info'
这里有几点要特别说明:
抽象基类中有的元数据,子模型没有的话,直接继承;
抽象基类中有的元数据,子模型也有的话,直接覆盖;
子模型可以额外添加元数据;
抽象基类中的 abstract=True 这个元数据不会被继承。也就是说如果想让一个抽象基类的子模 型,同样成为一个抽象基类,
那你必须显式的在该子模型的Meta中同样声明一个 abstract = True ;
有一些元数据对抽象基类无效,比如 db_table ,首先是抽象基类本身不会创建数据表,
其次它的 所有子类也不会按照这个元数据来设置表名。
由于Python继承的工作机制,如果子类继承了多个抽象基类,则默认情况下仅继承第一个列出的基 类的 Meta 选项。
如果要从多个抽象基类中继承 Meta 选项,必须显式地声明 Meta 继承。例如:
from django.db import models class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() class Meta: abstract = True ordering = ['name'] class Unmanaged(models.Model): class Meta: abstract = True managed = False class Student(CommonInfo, Unmanaged): home_group = models.CharField(max_length=5) class Meta(CommonInfo.Meta, Unmanaged.Meta): pass
警惕 related_name 和 related_query_name 参数
如果在你的抽象基类中存在 ForeignKey 或者 ManyToManyField 字段,并且使用了 related_name 或者 related_query_name 参数,
那么一定要小心了。因为按照默认规则,每一个子类都将拥有同样的字 段,这显然会导致错误。为了解决这个问题,
当你在抽象基类中使用 related_name 或者 related_query_name 参数时,它们两者的值中应该包含 %(app_label)s 和 %(class)s 部分:
%(class)s 用字段所属子类的小写名替换
%(app_label)s 用子类所属app的小写名替换
例如,对于 common/models.py 模块: from django.db import models class Base(models.Model): m2m = models.ManyToManyField( OtherModel, related_name="%(app_label)s_%(class)s_related", related_query_name="%(app_label)s_%(class)ss", ) class Meta: abstract = True class ChildA(Base): pass class ChildB(Base): pass
对于另外一个应用中的 rare/models.py : from common.models import Base class ChildB(Base): pass
对于上面的继承关系:
common.ChildA.m2m 字段的 reverse name (反向关系名)应该是 common_childa_related ;reverse query name (反向查询名)应该是 common_childas 。
common.ChildB.m2m 字段的反向关系名应该是 common_childb_related ;反向查询名应该是common_childbs 。
rare.ChildB.m2m 字段的反向关系名应该是 rare_childb_related ;反向查询名应该是rare_childbs 。
当然,如果你不设置 related_name 或者 related_query_name 参数,这些问题就不存在了
27:Django用户管理 【07】
后面我们在开发 登录功能 的时候需要数据库来保存 用户登录 信息和状态
Django中有个内置app 名为 django.contrib.auth ,缺省包含在项目 Installed App 设置中
这个app 的 models 定义中包含了一张 用户表,名为 auth_user
当我们执行 migrate 创建数据库表时,根据,就会为我们创建 用户表 auth_user ,如下所示
django.contrib.auth 这个 app 已经 为我们做好了登录验证功能
自定义用户模型:
Django 内置模块 contrib.auth 帮我们预置了一张用户表 user ,包含的字段有:
如果以上字段足够我们在项目中使用,那么我们直接使用默认User模型即可
但是如果他不符合我们的需求,我们要重新定义,怎么办?
比如,这个项目的用户表需要新增一些字段,像 真实姓名、手机号、用户类型等。
千万不要去改 Django 内置模块 contrib.auth.models 里面的类定义。(想一想,为什么?)
(如果改动源码,那么你的项目就依赖当前库,如果更新了库或者环境移植重写装库,那你原来的代码 就失效了)
一种推荐的方式是:通过继承 django.contrib.auth.models 里面的 AbstractUser 类的方式
在你项目文件的 models 里面进行如下定义:
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPE=( (0,'开发'), (1,'测试'), (2,'运维'), (3,'项目经理'), ) realname = models.CharField('真实姓名',max_length=30) phone = models.CharField('手机号',max_length=11,unique=True,null=True,blank=True) user_type = models.SmallIntegerField('用户类型 ',choices=USER_TYPE,default=1)
这样就在原来的 contrib.auth 里面的 user 表的基础上新增了 usertype、realname、phone、 这些字段
Django自定义验证:
然后,你需要告诉Django,使用这个 二次重构的 表作为 系统的 user表。
就是 在 settings.py 中,添加如下设置:
AUTH_USER_MODEL = 'sqtp.User'
其中 sqtp 为你的 User 定义 所在的 django app 名称
一定要确保 这个 sqtp 在你的 INSTALLED_APPS 里面设置了
接下来同步数据库:
python manage.py makemigrations sqtp
python manage.py makemigrations migrate
可能会遇到:
The field admin.LogEntry.user was declared with a lazy reference to 'sqtp.user', but app 'sqtp' doesn't provide model 'use r'.
这个错误是由于原有的用户模型被依赖,使用自定义会改变依赖关系,这个改动并不能自动完成,需要 手动修复你的架构,
将数据从旧的用户表移出,并有可能需要手动执行一些迁移操作
由于 Django 对可交换模型的动态依赖特性的限制, AUTH_USER_MODEL 所引用的模型必须在其应用的 第一次迁移中创建(通常称为 0001_initial );
否则,你会出现依赖问题
通俗点的就是:
1.删除migrations目录所有文件,
2.删除数据库并重写创建。然后再执行迁移动作就可 以了。
引用User模型:
用户模型创建好之后,其他模型需要关联的部分就可以加上了
首先是Project,增加项目 管理员 和 成员,由于关联了同一个模型,所以不要忘了设置反向查询名 related_name
如果我们直接引用 User 模型,那么以后我们的项目改成不同的用户模型时就无法自动切到 AUTH_USER_MODEL 配置的用户模型,
代码的可复用性就大打折扣了。因此应该直接引 用 AUTH_USER_MODEL 配置的用户模型,方法如下:
from django.conf import settings from .base import CommonInfo class Project(CommonInfo): proj_status = ( (0, 'developing',), (1, 'operating',), (2, 'stable',) ) # 管理员 admin = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True,related_name='project') # 成员 members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='projects') ...
通用表中的创建者和更新者字段: from django.db import models from django.conf import settings # 公共抽象模型 class CommonInfo(models.Model): # 创建者 create_by = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.SET_NULL,null=True,verbose_name='创建者', related_name='create_by') # 更新者 update_by = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.SET_NULL,null=True,verbose_name='更新者', related_name='update_by') ...
同步数据库时,出现错误: HINT: Add or change a related_name argument to the definition for 'sqtp.Project.update_by' or 'sqtp.Environment.update_by'. sqtp.Project.update_by: (fields.E305) Reverse query name for 'sqtp.Project.update_by' clashes with reverse query name for 'sqtp.Enviro nment.update_by'
这时由于父类的字段被子类继承,都使用了相同的反向查询名,者显然是不行的,
所以利用 % (app_label)s 和 %(class)s 让子类继承的字段可以自由替换反向查询名。
class CommonInfo(models.Model): # 创建者 create_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True,
verbose_name='创建者',related_name='%(app_label)s_%(class)s_create') # 更新者 update_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True,
verbose_name='更新者',related_name='%(app_label)s_%(class)s_update')
基础模型修改成这样然后 同步数据库:就没有问题了
python manage.py makemigrations sqtp
python manage.py migrate
27:Django用户管理视图开发 【07】
采用REST框架开发项目管理和用户管理: 创建序列器:serializers.py # 项目 class ProjectSerializer(serializers.ModelSerializer): class Meta: model = Project fields = '__all__' # 环境 class EnvironmentSerializer(serializers.ModelSerializer): class Meta: model = Environment fields = '__all__' # 用户 class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = '__all__'
创建视图: # 项目 class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer
# 环境 class EnvironmentViewSet(viewsets.ModelViewSet): queryset = Environment.objects.all() serializer_class = EnvironmentSerializer
# 用户 @api_view(['GET']) def user_list(request): queryset = User.objects.all() serializer = UserSerializer(queryset, many=True) return Response(serializer.data)
@api_view(['GET']) def user_detail(request, _id): try: req_obj = User.objects.get(pk=_id) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) # status提供常用http状态码 serializer = UserSerializer(req_obj) return Response(serializer.data)
创建路由: # 用户 urlpatterns = [ path('',include(router.urls)), # 引用相关路由 path('custom/',views.customer_api), path('users/',views.user_list), path('users/<int:_id>',views.user_detail), ]
28:Django 用户注册 【08】
用户注册功能--创建用户
不同系统有不同的注册流程,但是大体上都差不都,都是提交信息给服务器,然后服务器判断没有问题后创建用户。结合前端,我们当前的注册流程可以参考下图:
结合REST框架帮助我们完成验证的工作: # 注册序列器 class RegisterSerializer(serializers.ModelSerializer): # admin_code 不在User字段中,需要单独定义 admin_code = serializers.CharField(default='sqtp') # 字符串-sqtp class Meta: model = User fields = ['username','password','email','phone','realname','admin_code']
# 校验入参是否合法 def validate(self, attrs): # attrs为入参的字典形式 # 检查admin_code if 'admin_code' in attrs and attrs['admin_code']!='sqtp': raise ValidationError('错误的admin_code') return attrs
# 重写序列化器的保存方法 def register(self): in_param = self.data if 'admin_code' in in_param: in_param.pop('admin_code') # 创建用户数据不需要admin_code user=User.objects.create_superuser(**in_param) # 创建管理员 else: user=User.objects.create_user(**in_param) # 创建普通用户 return user
# 注册视图 @api_view(['POST']) def register(request): # 获取注册信息--序列化器 serializer = RegisterSerializer(data=request.data) if serializer.is_valid(): # 自动根据模型定义来判断数据入参是否合法 user = serializer.register() auth.login(request, user) # 登录 return Response(status=status.HTTP_201_CREATED, data={'msg': 'register success', 'is_admin': user.is_superuser, 'retcode':status.HTTP_201_CREATED}) return Response({'msg': 'error', 'retcode': status.HTTP_400_BAD_REQUEST, 'error': serializer.errors},status=status.HTTP_400_BAD_REQUEST)
注册URL: path('register/',views.register),
29:Django 用户登录 【08】
登录流程可参考下图,同样数据验证部分可以写进序列化器中:
登录序列器: 与注册不同的是,登录不需要默认的 序列化器 校验数据,因为登录使用的是 已有的数据,因此应该序列号其调用 validate 方法返回验证后的user对象即可 # 登录序列器 class LoginSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['username', 'password'] def validate(self, attrs): # 检查必填参数 position_params = ['username', 'password'] for param in position_params: if param not in attrs: raise ValidationError(f'缺少参数:{param}') # 验证用户名和密码 user = auth.authenticate(**attrs) if not user: raise ValidationError('用户名或密码不正确') return user
登录视图: @api_view(['POST']) def login(request): # 获取登录信息--序列化器 serializer = LoginSerializer(data=request.data) user = serializer.validate(request.data) if user: auth.login(request, user) return Response(status=status.HTTP_302_FOUND, data={'msg': 'login success', 'to': 'index.html'}) return Response({'msg': 'error', 'retcode': status.HTTP_400_BAD_REQUEST, 'error': serializer.errors},status=status.HTTP_400_BAD_REQUEST)
登录URL: path('login/',views.login),
30:渲染器-异常捕获重写 【08】
异常返回的信息需要重新处理: from rest_framework.exceptions import ValidationError from rest_framework.views import exception_handler #REST框架异常处理器 # 处理异常返回 def my_exception_handler(exc,context): response = exception_handler(exc,context) # 获取标准的错误响应 if response: # 成功捕获到异常 if isinstance(exc,ValidationError): error_msg = exc.detail[0] else: error_msg = exc response.data ={'msg':'error','retcode':response.status_code,'error':str(error_msg)} return response
31:用户登出 【08】
这个比较简单,没有请求参数,范围对应的视图就可以完成登出流程,因此无需序列化器
登出视图: @api_view(['GET']) def logout(request): # 当前用例处于登录状态 if request.user.is_authenticated: auth.logout(request) # 清除登录信息 return Response(status=status.HTTP_302_FOUND, data={'msg': 'logout', 'to': 'login.html'})
登出URL: path('logout/',views.logout),
32:获取当前用户信息视图 【08】
前后端分离的系统前端部分需要同步后端的访问状态,因此构造一个请求用于检查是否登录,
这样在切换页面时可以根据当前用户状态选择是否 重定向 到 登录界面用于跑马灯,以及前端判断当前用户是否登录的依据 @api_view(['GET']) def current_user(request): # 如果当前用户处于登录状态 if request.user.is_authenticated: serializer = UserSerializer(request.user) # 返回当前用户登录信息 return Response(serializer.data) else: return Response({'retcode': 403, 'msg': '未登录', 'to': 'login.html'},status=status.HTTP_403_FORBIDDEN)
32:用户状态校验 【08】
django默认的用户信息存储方案为 session 机制,有内置的装饰器可以验证当前请求是否携带用户认证信息 from django.contrib.auth.decorators import login_required @login_required def list_user(request): ... # 如果用户未登录就会被重定向到一个默认的URL(/accounts/login/) 状态码为403
但是如果我们用DRF,则可以用 DRF设置认证方案 可以使用 DEFAULT_AUTHENTICATION_CLASSES 设置全局的默认身份验证方案。比如: REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ) } 还可以使用基于 APIView 类视图的方式,在每个view或每个viewset基础上设置身份验证方案 from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView class ExampleView(APIView): authentication_classes = (SessionAuthentication, BasicAuthentication) permission_classes = (IsAuthenticated,) def get(self, request, format=None): content = { 'user': unicode(request.user), # `django.contrib.auth.User` 实例。 'auth': unicode(request.auth), # None } return Response(content) 或者,如果你使用基于函数的视图,那就使用 @api_view 装饰器。 @api_view(['GET']) @authentication_classes((SessionAuthentication, BasicAuthentication)) # 认证类 @permission_classes((IsAuthenticated,)) # 权限类 def example_view(request, format=None): content = { 'user': unicode(request.user), # `django.contrib.auth.User` 实例。 'auth': unicode(request.auth), # None } return Response(content)
另外,DRF 的认证是在定义有 权限类(permission_classes)的视图下才有作用,且 权限类(permission_classes)必须要求 认证用户 才能访问此视图。
如果没有定义权限类(permission_classes),那么也就意味着允许 匿名用户的访问,自然牵涉不到 认证相关 的限制了。
所以,一般在项目中的使用方式是在全局配置 DEFAULT_AUTHENTICATION_CLASSES 认证,然后会定义多个 base views,
根据不同的访问需求来继承不同的base views即可
DRF权限方案:
我们已经在DRF的 认证方案 中看到了 如何使用权限,接下来 扩展下权限的范围,只有当前项目的管理员才有读写当前项目的权限
定义权限: sqtp/permissions 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
加入 IsOwnerOrReadOnly 权限: class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer # 权限 authentication_classes = (SessionAuthentication, BasicAuthentication) permission_classes = (IsAuthenticated,IsOwnerOrReadOnly)
使用非当前项目的管理员用户进行修改测试,发现被拒绝: { "msg": "error", "retcode": 403, "error": "You do not have permission to perform this action." }
33:用例执行系统整体思路 【09】
首先目前 hr3 执行用例采取的方法是将 json 文件转化成 py 文件再用 pytest 去运行,我们可以做的就是将数据库中用例数据取出,再转化成对应格式的json文件即可。
用例的增删改查问题:
目前用例 以及 其他 模型字段都发生了改变,和现有业务不符,需要进行修改。
用例增加: 首先序列化器部分的展示数据需要进行变动: class CaseSerializer(serializers.ModelSerializer): config = ConfigSerializer() # 只需要序列化输出,则定义read_only = True teststeps = StepSerializer(many=True, ) # 以列表形式展示 many=True class Meta: model = Case fields = ['config', 'teststeps']
前端人员在架构师的指示下适配好了页面,根据API规则发送对应的数据: {desc: "123", name: "case001", project_id: "1"} 收到服务器异常: rest_framework.exceptions.ValidationError: {'teststeps':[ErrorDetail(string='This field is required.', code='required')]}
默认的情况下REST框架会调用自带的验证机制,当我们传递的数据和序列化器存在出入时,框架会抛出异常
teststeps字段不作为接口的入参,只做为出参,可以设置 read_only = True,这样在校验入参时,不会因为没有传递teststeps而报错了
# 用例 序列化器 class CaseSerializer(serializers.ModelSerializer): config = ConfigSerializer() # 只需要序列化输出,则定义read_only = True teststeps = StepSerializer(many=True, read_only=True) # 以列表形式展示many=True ...
新增数据调用的是序列化器的create方法,如果默认的不满足要求就要重新父类方法: # 新增用例 class CaseSerializer(serializers.ModelSerializer): config = ConfigSerializer() # 只需要序列化输出,则定义read_only = True teststeps = StepSerializer(many=True, read_only=True) # 以列表形式展示many=True project_id = serializers.CharField(write_only=True) # 只需要反序列化输入
def create(self, validated_data): config_kws = validated_data.pop('config') # 从请求参数弹出config参数用于创建config project = Project.objects.get(pk=validated_data['project_id']) # pk:主键查找 project config = Config.objects.create(project=project,**config_kws) #注意关联 project file_path = f'{project.name}_{config.name}.json' # 项目名+用例名.json instance = Case.objects.create(file_path=file_path, config=config) return instance
class Meta: model = Case fields = ['config', 'teststeps','project_id','file_path']
# 修改:接下来,定制化修改数据的方法,修改采用的是序列化器的update方法 def update(self, instance, validated_data): config_kws = validated_data.pop('config') # 从请求参数弹出config参数用于创建config project = Project.objects.get(pk=validated_data['project_id']) config_kws['project'] = project # 此时instance为Case数据对象 conf_serializer=ConfigSerializer(instance=instance.config,data=config_kws) # 调用序列化器更新config数据 if conf_serializer.is_valid(): conf_serializer.save() else: raise ValidationError(conf_serializer.errors) # 更新case数据 instance.file_path = validated_data['file_path'] return instance
如果要连同 步骤 一起更新 则需要 更新与之相关 的序列化器: class StepSerializer(serializers.ModelSerializer): request = RequestSerializer() belong_case_id = serializers.IntegerField(write_only=True,required=False) def create(self, validated_data): request_kws= validated_data.pop('request') serializer = RequestSerializer(data=request_kws) if serializer.is_valid(): req_obj=serializer.save() else: raise ValidationError(serializer.errors) step_obj = Step.objects.create(testrequest=req_obj,**validated_data) return step_obj class Meta: model = Step fields = ['name', 'variables', 'request', 'extract','validate','belong_case_id']
更新用例的update方法: class CaseSerializer(serializers.ModelSerializer): ... def update(self, instance, validated_data): config_kws = validated_data.pop('config') # 从请求参数弹出config参数用于创建config project = Project.objects.get(pk=validated_data['project_id']) config_kws['project'] = project
# 此时instance为Case数据对象 conf_serializer=ConfigSerializer(instance=instance.config,data=config_kws) # 调用序列化器更新config数据 if conf_serializer.is_valid(): conf_serializer.save() else: raise ValidationError(conf_serializer.errors)
# 更新step数据 很多 step 更新 steps_kw = validated_data.pop('teststeps') for kw in steps_kw: kw['belong_case_id']=self.data['id'] ss=StepSerializer(data=kw) if ss.is_valid(): ss.save() else: raise ValidationError(ss.errors)
# 更新case数据 instance.file_path = validated_data['file_path'] return instance ...
删除
暂时使用框架自带的功能
查询 前端展示需要 project 的数据,需要从 config 中获取,因此 config 的 project 数据需要嵌套展示 class ConfigSerializer(serializers.ModelSerializer): project = ProjectSerializer(required=False) class Meta: model = Config fields = ['name', 'base_url', 'variables', 'parameters', 'verify', 'project']
34:序列化器中的类属性字段 【09】
序列化中所定义的类属性字段,一般情况下与 模型类字段 相对应 默认情况下,这些类属性字段既可以进行 序列化 输出,也可以进行 反序列化 输入 不需要输入(反序列化)、输出(序列化)的字段,则不需要定义,定义的字段则必须出现在fields列表中 只需要反序列化输入:则定义write_only = True 只需要序列化输出:则定义read_only = True 响应的参数如果是多个查询集:需要在JsonResponse()中传参many=True label:当前字段在前端的api页面中所显示的 字段名称 allow_null = False:当前字段是否允许传 None,默认是False(必填字段False,反之则True) allow_blank = False:当前字段是否运行为空,默认是False(必填字段False,反之则True) required=False:当前字段允许不传,默认是True(必填字段True,反之则False)
35:反序列化_校验机制 【09】
调用 序列化器对象 的 is_valid() 方法,校验 前端参数的正确性,不调用则不校验,校验成功返回True、校验失败返回False is_valid(raise_exception = True):校验失败后,则抛出异常 当调用is_valid()之后,才能 调用序列化器对象的 errors 属性,内容为校验的错误提示(dict) 在 views.py 中,如果传参进行了 输入反序列化 的话,那么需要调用的是经过校验后的数据,
比如说新增数据,应该是:xxx类.objects.create(**serializer.validated_data) 在视图集(ViewSet)中,REST都默认调用了is_valid()方法来校验入参
35:模型与序列化器更新 【10】
修改 Config 模型: 去掉name字段的唯一约束: name = models.CharField('名称',max_length=128,)
修改Request模型: 为了和 hr3 的字段名称保持统一,更改reqeust针对step的反向查询名由 testrequest 为 request: # models/hr3.py class Request(CommonInfo): ... step = models.OneToOneField(Step, on_delete=models.CASCADE, null=True, related_name='request') ...
修改Step模型: 除此以外,步骤需要1个 sorted_no 字段用于步骤 的 顺序排列 现在模型添加排序字段, 并且增加联合约束和修改排序字段 #models/hr3.py class Step(models.Model): # 同个模型中,两个字段关联同1个模型,必须指定related_name,且名字不能相同 # 属于哪个用例 ... sorted_no = models.PositiveSmallIntegerField('步骤顺序', default=1) class Meta: verbose_name = '测试步骤表' ordering = ['sorted_no','id'] # 先根据sorted_no排序,再根据id排序 unique_together = ['belong_case', 'sorted_no'] # 同1个用例的步骤顺序应该是不一样的
同步数据库:
python manage.py makemigrations
python manage.py migrate
更新Case序列化器: 前端页面完成了详情页的迭代,现在可以展示用例详情页了,为了配合前端页面展示用例详情,我们需要把用例的ID,创建时间和更新时间进行展示 fields加上 'create_time', 'update_time' # serializers/hr3.py class CaseSerializer(serializers.ModelSerializer): ... class Meta: model = Case fields = ['id','config', 'teststeps', 'desc', 'project_id', 'file_path', 'create_time', 'update_time']
更新Step序列化器: 1:Step序列化器增加排序字段,更新 belong_case为 belong_case_id, 2:构造步骤代码部分 参数 testrequest 改成 request 3:更新fields字段,只展示需要展示的信息 # serializers/hr3.py class StepSerializer(serializers.ModelSerializer): testrequest = RequestSerializer() belong_case_id = serializers.IntegerField(write_only=True, required=False) # 只写 def create(self, validated_data): # 构造请求 req_kws = validated_data.pop('request') req_serializer = RequestSerializer(data=req_kws) if req_serializer.is_valid(): req_obj = req_serializer.save() else: raise ValidationError(req_serializer.errors) # 构造步骤 step_obj = Step.objects.create(request=req_obj, **validated_data) return step_obj class Meta: model = Step fields = ['name', 'variables', 'request', 'extract', 'validate', 'setup_hooks', 'teardown_hooks','belong_case_id','sorted_no']
Case编辑功能补充: 修改common部分无法更新的问题: 原 case 更新功能只能更新 config 部分内容,common 部分无法更新,经过查看是 instance设置后没有调用save()同步到数据库 class CaseSerializer(serializers.ModelSerializer): ... # 修改用例 def update(self, instance, validated_data): ... # 利用python反射自动赋值 for k, v in validated_data.items(): # 注意validated_data不要包含instance数据对象没有的字段参数 setattr(instance, k, v) instance.save() # 记得调用save() return instance
完成Step创建关联本用例: 这里记得更新当天的前端文件,否则无法生成步骤序号,以及所属用例ID,修改用例部分,线删除关联的步骤,再重新创建 # 修改用例 def update(self, instance, validated_data): ''' instance 当前被修改的数据对象 validated_data 校验后的入参--字典形式 ''' ... # teststeps更新 # 删除当前用例下的所有step steps = instance.teststeps.all() for step in steps: step.delete() teststeps = validated_data.pop('teststeps') for step in teststeps: # 取出步骤关联的用例ID step['belong_case'] = self.instance.id ss = StepSerializer(data=step) if ss.is_valid(): ss.save() else: raise ValidationError(ss.errors) ...
完成Request数据关联Step: 发现Request数据创建了,但是并没有成功关联上Step,更改步骤序列化器的创建方法 # serializers/hr3.py class StepSerializer(serializers.ModelSerializer): request = RequestSerializer() belong_case_id = serializers.IntegerField(required=False) def create(self, validated_data): req_kws = validated_data.pop('request') # 构造步骤 step_obj = Step.objects.create(**validated_data) # 构造请求 req_kws['step_id']=step_obj.id req_serializer = RequestSerializer(data=req_kws) if req_serializer.is_valid(): req_obj = req_serializer.save() else: raise ValidationError(req_serializer.errors) return step_obj
修改请求序列化器: # 请求模型的序列化器 class RequestSerializer(serializers.ModelSerializer): method = serializers.SerializerMethodField() # 1.自定义字段获取方法 step_id = serializers.IntegerField(write_only=True, required=False) # 不要求请求入参携带此参数 # 2.配套方法 def get_method(self, obj): # rest框架获取method时,会自动调用该方法 return obj.get_method_display() # 返回choice的displayname class Meta: model = Request # 指定对应的模型 fields = ['step_id', 'method', 'url', 'params', 'headers', 'json', 'data'] #请求数据显示的内容较多,剔除不必要的字段
36:数据与用户关联 【10】
我们目前创建的数据并没有和 用户 产生关联,用户 并不是作为序列化 参数传入序列化器的,
而是通过请求的user属性获取的(user不在传过来的数据中,而是通过request.user获得) 我们处理的方式是在我们的代码片段 视图 中重写一个 .perform_create() 方法,
这样我们可以修改实例保存的方法,并处理传入请求或请求URL中隐含的任何信息。
在视图类中,添加这个方法: def perform_create(self, serializer): serializer.save(creator=self.request.user) 同理,更新功能,可以增加以下函数 def perform_update(self, serializer): serializer.save(updater= self.request.user)
creator表示创建者字段,updater表示更新者字段,二者都要出现在数据模型中
同时记得序列化器 fileds 里需要增加 creator 和 updater 对应的字段,否则不生效。 以 CaseSerializer 为例,增加对应的字段 class CaseSerializer(serializers.ModelSerializer): config = ConfigSerializer() # config字段就对应其序列化器返回的内容 teststeps = StepSerializer(required=False,many=True)
# read_only=True为只读参数,required=False 表示非必填,就不会校验入参 many=True展示为列表形式 project_id = serializers.CharField(write_only=True) # 只做为入参 create_by = UserSerializer(write_only=True,required=False) updated_by = UserSerializer(write_only=True,required=False) class Meta: model = Case fields = ['config','teststeps','project_id','desc','id','file_path',
'create_time','update_time','create_by','updated_by'] #序列化器定义的字段必须再此展示
# 覆盖父类新增反方法 def create(self, validated_data): ''' validated_data: 校验后的入参--字典形式 ''' # 创建config config_kws = validated_data.pop('config') # 取出config参数 project = Project.objects.get(pk=validated_data.pop('project_id')) config =Config.objects.create(project=project,**config_kws) #关联project # 创建用例 file_path = f'{project.name}_{config.name}.json' # 项目名+用例名.json case = Case.objects.create(config=config,file_path=file_path,**validated_data) return case
37:新增项目没有管理员的问题 【10】
配合前端,入参增加admin_id字段,只写模式 class ProjectSerializer(serializers.ModelSerializer): admin_id = serializers.IntegerField(write_only=True) admin = UserSerializer() create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) # 格式化输出时间 class Meta: model = Project fields = ['id', 'admin_id', 'admin', 'name', 'desc', 'status', 'version', 'create_time', 'update_time']
38:用例文件生成 【10】
用例数据我们采用序列化器中保存好的json数据: # 用例 class CaseSerializer(serializers.ModelSerializer): # 生成json文件 def to_json_file(self,path=None): if path is None: path = self.instance.file_path if not path.endswith('.json'): path = path + '.json' path = f'testcase/{path}' # 生成json文件 content = JSONRenderer().render(self.data) with open(path,'wb',) as f: f.write(content) return path 该方法目前只会忠实的把 数据库 中的用例 数据 以 json 格式输出到文件上,
如果Json格式不符合hr3规范,也是不能生成用例的。后面我们会把用例数据的准入规范做起来。
39:HR3入参数据校验器 【11】
目前json用例文件在转化成py文件过程中提示文件内容不合法 案例中提示的错误: 2021-06-25 10:57:37.989 | WARNING | httprunner.make:__make:567 - Invalid testcase file:
D:\Course\course_django\autotpsite_day8\testcase\荣耀王者_test.jsonTestCaseFormatError: Invalid teststep validate: {}
原因是由于"validate"字段应该是 []而不是 {},对照下课程1的文档,里面有各个字段的类型定义规 范。
想在源头杜绝这个事情,就要在 保存 和 更新数据时,校验入参,如果不合法就拒绝保存
自己写过滤器来校验数据 当字段或 模型的规则 不足以验证 入参 数据时,我们需要自定义规则来做数据验证 ConfigSerializer: # 配置 class ConfigSerializer(serializers.ModelSerializer): project = ProjectSerializer(read_only=True) class Meta: model = Config fields = '__all__' def validate(self, attrs): if 'variables' in attrs and not isinstance(attrs['variables'], dict): raise ValidationError('请传递正确的variables格式:dict') if 'parameters' in attrs and not isinstance(attrs['parameters'], dict): raise ValidationError('请传递正确的parameters格式:dict') if 'export' in attrs and not isinstance(attrs['export'], list): raise ValidationError('请传递正确的export格式:dict') return attrs ...
StepSerializer: # 步骤模型的序列化器 class StepSerializer(serializers.ModelSerializer): request = RequestSerializer() belong_case_id = serializers.IntegerField(write_only=True, required=False) # 只写 class Meta: model = Step fields = ['name', 'variables', 'request', 'extract', 'validate', 'setup_hooks', 'teardown_hooks','belong_case_id', 'sorted_no'] def validate(self, attrs): if 'variables' in attrs and not isinstance(attrs['variables'], dict): raise ValidationError('请传递正确的variables格式:dict') if 'extract' in attrs and not isinstance(attrs['extract'], dict): raise ValidationError('请传递正确的extract格式:dict') if 'validate' in attrs and not isinstance(attrs['validate'], list): raise ValidationError('请传递正确的validate格式:list') if 'setup_hooks' in attrs and not isinstance(attrs['setup_hooks'], list): raise ValidationError('请传递正确的setup_hooks格式:list') if 'teardown_hooks' in attrs and not isinstance(attrs['teardown_hooks'], list): raise ValidationError('请传递正确的teardown_hooks格式:list') if 'request' in attrs and not isinstance(attrs['request'], dict): raise ValidationError('请传递正确的request格式:dict') return attrs ...
RequestSerializer: # 请求模型的序列化器 class RequestSerializer(serializers.ModelSerializer): method = serializers.SerializerMethodField() # 1.自定义字段获取方法 step_id = serializers.IntegerField(write_only=True, required=False) # 不要求请求入参携带此参数 # 2.配套方法 def get_method(self, obj): # rest框架获取method时,会自动调用该方法 return obj.get_method_display() # 返回choice的displayname class Meta: model = Request # 指定对应的模型 fields = ['step_id', 'method', 'url', 'params', 'headers', 'json', 'data'] # fields = '__all__' # 显示对应模型的所有字段 # 定义可以显示和操作的字段 def validate(self, attrs): if 'params' in attrs and not isinstance(attrs['params'], dict): raise ValidationError('请传递正确的params格式:dict') if 'headers' in attrs and not isinstance(attrs['headers'], dict): raise ValidationError('请传递正确的headers格式:dict') if 'cookies' in attrs and not isinstance(attrs['cookies'], dict): raise ValidationError('请传递正确的cookies格式:dict') return attrs
CaseSerializer: # 用例 class CaseSerializer(serializers.ModelSerializer): config = ConfigSerializer() # 显示指定config字段为序列化对象,REST会自动提取其值 teststeps = StepSerializer(many=True, required=False) # 以列表形式展示many=True project_id = serializers.CharField(write_only=True) # project_id只用于输入 create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) update_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True) # 格式化输出时间 class Meta: model = Case fields = ['id', 'config', 'teststeps', 'desc', 'project_id', 'file_path', 'create_time', 'update_time'] def validate(self, attrs): # 校验config 和 teststeps if 'config' in attrs and not isinstance(attrs['config'], dict): # 如果格式不符合要求 raise ValidationError('请传递正确的config格式:dict') if 'teststeps' in attrs and not isinstance(attrs['teststeps'], list): # 如果格式不符合要求 raise ValidationError('请传递正确的teststeps格式:list') return attrs ...
40:用例文件输出过滤 【11】
目前生成用例文件,一些不必要的字段也显示到了文件上: { "config": { "project": { "id": 1, "admin": null, "name": "测试开发3", "status": 0, "version": "v3", "desc": "测试开发", "create_time": "2021-11-06 08:57:56", "update_time": "2021-11-06 08:57:56" }, "name": "case003", "base_url": "", "variables": null, "parameters": null, "export": null, "verify": false },
如果文件中的字段不属于HR3,或者HR3对应的字段为 空字符串,空列表,空字典等,则不输出到用例 文件中。
我们可以自定义一个过滤器,过滤后的字段再输出到用例文件中(jsonfile)
def filter_data(self,data): template = { 'config': { 'name': str, 'base_url': str, 'variables': dict, 'parameters': dict, 'verify': bool, 'export': list }, 'teststeps': [{ 'name': str, 'variables': list, 'extract': dict, 'validate': list, 'setup_hooks': list, 'teardown_hooks': list, 'request': { 'method': str, 'url': str, 'params': list, 'headers': dict, 'cookies': dict, 'data': dict, 'json': dict }, }] } return self.merge_dict(template,data)
字典同步: def merge_dict(self,left, right): # 覆盖左侧同类项 for k in right: if k in left: if isinstance(left[k], dict) and isinstance(right[k], dict): self.merge_dict(left[k], right[k]) elif isinstance(left[k], list) and isinstance(right[k], list): # 左右元素数量保持一致 item = copy.deepcopy(left[k][0]) left[k] = [item for a in right[k]] for idx, one in enumerate(right[k]): le = left[k][idx] self.merge_dict(le, one) elif right[k]: # 合并的条件: 取交集,且right不为空(也包含空字符串,空字典和列表) left[k] = right[k] elif not right[k]: left.pop(k) # 去除左侧多余项 for k in list(left.keys()): if k not in right: left.pop(k) return left
重新执行:GET/cases/{id}/run_case/
查看生成的用例文件中,应该去除掉多余的字段
41:单用例执行 【11】
用例文件生成后,我们就可以采用 hrun3 方法来执行用例了 hr底层采用Pytest, pytest.main方法的返回值code代表不同的结果:
Exit code 0 All tests were collected and passed successfully 所有测试均已收集并成功通过
Exit code 1 Tests were collected and run but some of the tests failed 收集并运行了测试,但有些测试失败了
Exit code 2 Test execution was interrupted by the user 测试执行被用户中断
Exit code 3 Internal error happened while executing tests 执行测试时发生内部错误
Exit code 4 pytest command line usage error pytest 命令行使用错误
Exit code 5 No tests were collected 未收集测试
from httprunner.cli import main_run class CaseViewSet(viewsets.ModelViewSet): queryset = Case.objects.all() serializer_class = CaseSerializer @action(methods=['GET'],detail=True,url_path='run',url_name='run_case') def run_case(self,request,pk): case = Case.objects.get(pk=pk) # 根据id获取用例 serializer = self.get_serializer(instance=case) # 调用序列化器 path = serializer.to_json_file() # 生成用例文件 # hr3运行测试用例 exit_code = main_run([path]) # 入参是列表=pytest参数包括用例文件路径,Pytest命令选项 # 只要exit_code不是0,就是执行失败了 if exit_code != 0: return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data={'error': 'failed run case', 'retcode': exit_code}) return Response(data={'retcode': status.HTTP_200_OK, 'msg': 'run success'})
42:多用例执行 【11】
模型设计: 多用例我们可以 采用 测试计划 关联 测试用例,然后 # models/task.py from django.conf import settings from django.db import models from .base import CommonInfo from .hr3 import Case from .mgr import Environment class Plan(CommonInfo): status_choice = ( (0, '未执行'), (1, '执行中'), (2, '中断'), (3, '已执行'), ) cases = models.ManyToManyField(Case, verbose_name='测试用例', blank=True) # 执行者 executor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, verbose_name='执行者') # 测试环境 environment = models.ForeignKey(Environment, on_delete=models.SET_NULL, null=True, verbose_name='测试环境') name = models.CharField('计划名称', max_length=32, unique=True) status = models.SmallIntegerField(choices=status_choice, default=0, verbose_name='计划状态') exec_counts = models.PositiveSmallIntegerField(default=0, verbose_name='执行次数') # models/__init__.py from .task import Plan # 模块文件记得对外暴露 同步数据库: python manage.py makemigrations python manage.py migrate 序列化器设计: serializers/task.py class PlanSerializer(serializers.ModelSerializer): class Meta: model = Plan fields = '__all__' # models/__init__.py from .task import Plan # 模块文件记得对外暴露
视图设计: class PlanViewSet(viewsets.ModelViewSet): queryset = Plan.objects.all() serializer_class = PlanSerializer
视图模块化: views/auth.py """ @date: 2021/11/9 @file: auth.py """ from django.contrib import auth from rest_framework import status from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.decorators import api_view, authentication_classes, permission_classes, action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from sqtp.models import User from sqtp.serializers import UserSerializer, RegisterSerializer, LoginSerializer
@api_view(['GET']) @authentication_classes((BasicAuthentication,SessionAuthentication)) @permission_classes((IsAuthenticated,)) def user_list(request): query_set = User.objects.all() serializer = UserSerializer(query_set, many=True) return Response(serializer.data)
@api_view(['GET']) def user_detail(request, _id): try: user = User.objects.get(pk=_id) except User.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) serializer = UserSerializer(instance=user) return Response(serializer.data)
#注册用户 @api_view(['POST']) @permission_classes(()) def register(request): # 获取序列化器 serializer = RegisterSerializer(data=request.data) if serializer.is_valid(): #根据序列器和模型字段综合检查数据是否合法 user = serializer.register() #创建用户数据 auth.login(request,user) # 完成用户登录状态设置 return Response(data={'msg':'register success','is_admin':user.is_superuser,'retcode':status.HTTP_201_CREATED},status=status.HTTP_201_CREATED) return Response(data={'msg':'error','retcode':status.HTTP_400_BAD_REQUEST,'error':serializer.errors},status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST']) @permission_classes(()) def login(request): # 获取登录信息--序列化器 serializer = LoginSerializer(data=request.data) user = serializer.validate(request.data) if user: auth.login(request,user) #登录存储session信息 return Response(data={'msg':'login success','to':'index.html'},status=status.HTTP_302_FOUND) return Response(data= {'msg':'error','retcode':status.HTTP_400_BAD_REQUEST,'error':serializer.errors}, status=status.HTTP_400_BAD_REQUEST) @api_view(['GET']) def logout(request): if request.user.is_authenticated: #如果当前用户处于登录状态 auth.logout(request) #登出,清除session return Response(data={'msg':'logout success','to':'login.html'},status=status.HTTP_302_FOUND)
#当前用户信息 @api_view(['GET']) @permission_classes(()) def current_user(request): if request.user.is_authenticated: #如果当前用户处于登录状态 # 返回当前用户信息 serializer = UserSerializer(request.user) return Response(data=serializer.data) else: return Response(data={'retcode':403,'msg':'未登录','to':'login.html'},status=403)
views/hr3.py from httprunner.cli import main_run from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from sqtp.models import Request, Case, Step from sqtp.serializers import RequestSerializer, CaseSerializer, StepSerializer from drf_yasg.utils import swagger_auto_schema from rest_framework import viewsets from django.utils.decorators import method_decorator
# 优化3:视图集--增删改查 @method_decorator(name='list', decorator=swagger_auto_schema(operation_summary='列出数据', operation_description='列出请求数据...')) @method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='增加数据', operation_description='增加请求数据...')) @method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='查看详情', operation_description='查看单个请求数据...')) @method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='删除数据', operation_description='删除请求数据...')) @method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='更新数据', operation_description='更新请求数据...')) class RequestViewSet(viewsets.ModelViewSet): queryset = Request.objects.all() # 数据的查询集 serializer_class = RequestSerializer
class CaseViewSet(viewsets.ModelViewSet): queryset = Case.objects.all() serializer_class = CaseSerializer # 同步创建用户 def perform_create(self, serializer): serializer.save(create_by=self.request.user) # 同步更新用户 def perform_update(self, serializer): serializer.save(updated_by=self.request.user)
@action(methods=['GET'],detail=True,url_path='run',url_name='run_case') # 完整的url等于/cases/<int:case_id>/run def run_case(self,request,pk): # 获取序列化器 case = Case.objects.get(pk=pk) #根据ID获取当前用例 serializer = self.get_serializer(instance=case) path = serializer.to_json_file() # subprocess.Popen(f'hrun {path}',shell=True) #命令行执行法 # HR3 API执行法 exit_code=main_run([path]) # 根据推出代码判断是否执行成功 if exit_code !=0: return Response(data={'error':'failed run case','retcode':exit_code},status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(data={'msg':'run success','retcode':200})
class StepViewSet(viewsets.ModelViewSet): queryset = Step.objects.all() serializer_class = StepSerializer
views/mgr.py # Create your views here. from sqtp.models import Project, Environment from sqtp.permissions import IsOwnerOrReadOnly from sqtp.serializers import ProjectSerializer, \ EnvironmentSerializer from rest_framework import viewsets
class ProjectViewSet(viewsets.ModelViewSet): queryset = Project.objects.all() serializer_class = ProjectSerializer #权限 permission_classes = (IsOwnerOrReadOnly,)
class EnvironmentViewSet(viewsets.ModelViewSet): queryset = Environment.objects.all() serializer_class = EnvironmentSerializer #权限 #authentication_classes = (()) #传入空元组表示禁用全局认证 permission_classes = (())
views/task.py from sqtp.models import Plan from sqtp.serializers import PlanSerializer from rest_framework import viewsets class PlanViewSet(viewsets.ModelViewSet): queryset = Plan.objects.all() serializer_class = PlanSerializer # 定义运行测试计划方法,批量运行测试用例并生成测试报告 def run_plan(self): pass
views/init.py from .auth import user_list,user_detail,current_user,register,login,logout from .hr3 import CaseViewSet,RequestViewSet,StepViewSet from .mgr import ProjectViewSet,EnvironmentViewSet from .task import PlanViewSet
注册路由 sqtp.urls.py router.register(r'plans', views.PlanViewSet)
43:视图集自定义方法回顾 【11】
用例运行生成的方法 采用的是目前是单独设定的函数视图方法 # 用例运行 @api_view(['GET']) def run_case(request,_id): case = Case.objects.get(pk=_id) # 根据id获取用例 serializer = CaseSerializer(instance=case) # 调用序列化器 path = serializer.to_json_file() # 生成用例文件 return Response({'retcode': status.HTTP_200_OK, 'msg': path})
这样做不能 复用 视图集的 序列化器,而且URL不够统一,所以 改查 视图集中 自定义方法
视图集自定义action动作: 在视图集中,除了默认的动作(
list() 提供一组数据,
retrieve() 提供单个数据,
create() 创建数据,
update() 保存数据,
destory() 删除数据)外,
可以自定义动作添加自定义动作时,需要 rest_framework.decorators.action 装饰器 在自定义动作方法上,添加 @action(),接收两个参数methods,该action支持的请求方式,列表传递detail,
action中要处理的是否是视图资源的对象(即是否通过url路径获取主键) True 表示使用通过 URL 获取的主键对应的数据对象 False 表示不使用 URL 获取主键 class CaseViewSet(viewsets.ModelViewSet): queryset = Case.objects.all() serializer_class = CaseSerializer
@action(methods=['GET'],detail=True,url_path='run',url_name='run_case') def run_case(self,request,pk): case = Case.objects.get(pk=pk) # 根据id获取用例 serializer = self.get_serializer(instance=case) # 调用序列化器 path = serializer.to_json_file() # 生成用例文件 return Response({'retcode': status.HTTP_200_OK, 'msg': path})