测试平台开发 - 管理功能开发

模型关联关系

用例和项目之间的关联可以直接通过config,因为config是套件和用例的扩展,相当于用例和套件本身 。

一、 模块的包管理

1. 创建模型包

如果模型过多时,需要把模型分别存储。

在应用中创建一个名为 models 的包,将所有的模型文件全部存放在 该文件夹下, 删除原来的 models.py 文件。

2. 创建 模型文件

__init__.py 文件中,要使用 显示明确的方式导入每个模型,这样不会让命名混淆,便于可读。

1
2
3
4
5
6
# sqpt/models/__init__.py

from .hr3 import Request, Case, Config, Suite, Step
from .auth import User
from .base import CommonInfo
from .mgr import Environment, Project

hr3.py 存放所有核心数据模型

mgr.py存放所有项目管理相关模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# mgr.py

from django.db import models
from .base import CommonInfo
from django.conf import settings # 指向 django 项目的配置模块 settings.py


# 项目
class Project(CommonInfo):
PRO_STATUS = (
(0, '开发中'),
(1, '维护中'),
(2, '稳定运行'),
)
admin = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, related_name='admin_pros', verbose_name='管理员')
members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='member_pros', verbose_name='成员')
name = models.CharField('项目名称', max_length=256, unique=True)
status = models.SmallIntegerField('项目状态', choices=PRO_STATUS, default=0)
version = models.CharField('项目版本', default='v0.1', max_length=10)

class Meta(CommonInfo.Meta): # 需要显示继承元类,才会继承相关属性(abstract 除外)
verbose_name = '项目表'


# 测试环境
class Environment(CommonInfo):
service_type = (
(0, 'web服务器'),
(1, '数据库服务器'),
(2, '缓存服务器'),
)
service_os = (
(0, 'windows'),
(1, 'linux'),
)
service_status = (
(0, 'active'), # 可用状态
(1, 'disable'), # 不可以状态
)
project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='所属项目')
# django ORM 提供 GenericIPAddressField 专门用于存储 IP 字段
ip = models.GenericIPAddressField('IP地址', default='127.0.0.1')
port = models.CharField('端口号', max_length=5, default='8080')
category = models.SmallIntegerField('服务器类型', choices=service_type, default=0)
os = models.SmallIntegerField('操作系统', choices=service_os, default=1)
status = models.SmallIntegerField('服务器状态', choices=service_status, default=0)

class Meta(CommonInfo.Meta):
verbose_name = '测试环境表'

二、 模型继承

1. 抽象类

通友字段 用一个 抽象模型类 来存放,然后其他模型继承该抽象模型类,例如:创建时间、更新时间等

在元类中使用 abstract = True 定义 抽象类,抽象类 不会创建 数据库表

auto_now_add : 第1次创建数据时自动添加当前时间

auto_now :每次更新数据时自动添加当前时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# sqpt/models/base.py

from django.db import models
from django.conf import settings


class CommonInfo(models.Model):
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True,related_name='%(app_label)s_%(class)s_create')
updater = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True,related_name='%(app_label)s_%(class)s_update')

create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间', null=True)
decs = models.CharField('描述', max_length=1024, null=True, blank=True)

class Meta:
abstract = True # 定义为抽象模型,不会创建数据库
ordering = ['id']

子类继承父类:

1
2
3
4
5
6
# mgr.py
class Project(CommonInfo):
...

class Environment(CommonInfo):
...

2. 抽象基类(父类) 的 Meta 数据

如果子类没有声明自己的Meta类,那么它将自动继承抽象基类的Meta类。

如果子类要设置自己的Meta属性,则需要扩展基类的Meta:

1
2
3
4
5
6
7
8
9
10
11
12
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 继承。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# hr3.py

from django.db import models
from .base import CommonInfo
from .mgr import Project


class Config(models.Model):
project = models.ForeignKey(Project, on_delete=models.DO_NOTHING, null=True, verbose_name='所属项目')
name = models.CharField('名称', max_length=128)
# 可 null 可空白
base_url = models.CharField('IP/域名', max_length=512, null=True, blank=True)
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 Meta:
ordering = ['id'] # 根据 ID 正向排序(反向排序 ['-id'])
# db_table = ['conf_tb']


class Step(models.Model):
# 同模型中,两个字段关联一个模型时,需要指定 related_name, 且名字不能相同
# 属于哪个用例
belong_case = models.ForeignKey('Case', on_delete=models.CASCADE, related_name='test_steps')

# 引用那条用例
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(CommonInfo):
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

class Meta(CommonInfo.Meta):
ordering = ['id']
verbose_name = '接口请求表'


# 用例
class Case(CommonInfo):
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

class Meta(CommonInfo.Meta):
ordering = ['id']
verbose_name = '用例表'


# 套件
class Suite(CommonInfo):
config = models.OneToOneField(Config, on_delete=models.DO_NOTHING)
file_path = models.CharField('套件文件路径', max_length=1000, default='demo_suite.json')

def __str__(self):
return self.config.name

class Meta(CommonInfo.Meta):
ordering = ['id']
verbose_name = '套件表'

如果 抽象类中 存在 ForeignKey 或者 ManyToManyField 字段,那么使用 related_name 或者 related_query_name 参数的值就不能固定,否则在正向查询或反向查询时会出现错误。

解决办法:

related_name 或者 related_query_name 的值中包含 %(app_label)s%(class)s 部分

  • %(class)s : 用字段所属子类的小写名替换
  • %(app_label)s : 用子类所属app的小写名替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# sqpt/models/base.py

from django.db import models
from django.conf import settings


class CommonInfo(models.Model):
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True,related_name='%(app_label)s_%(class)s_create')
updater = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True,related_name='%(app_label)s_%(class)s_update')

create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间', null=True)
decs = models.CharField('描述', max_length=1024, null=True, blank=True)

class Meta:
abstract = True # 定义为抽象模型,不会创建数据库
ordering = ['id']
  • 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 。

三、 用户管理

1. 自定义用户模型

当我们执行 migrate 创建数据库表时,就会为我们创建 用户表 auth_user ,如下所示

如果模型中的字段不符合我们的需求时,通过继承 django.contrib.auth.models 里面的 AbstractUser 类的方式,在你项目文件的 models 里面进行如下定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# models/auth.py

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=32, null=True)
phone = models.CharField('手机号', max_length=11, unique=True)
user_type = models.SmallIntegerField('用户类型', choices=USER_TYPE, default=1)

这样就在原来的 contrib.auth 里面的 user 表的基础上,新增了 usertyperealnamephone、 这些字段

settings.py 文件中 设置,使 Django 以该表作为 user表

1
2
3
4
# settings.py

# 自定义用户模型
AUTH_USER_MODEL = 'sqpt.User'

同步数据库模型前,需要:

1.删除 migrations 目录及所有文件,

2.删除数据库并重写创建。

3.再执行数据库同步。

2. 引用 User 模型

直接引用User模型,那么以后我们的项目改成不同的用户模型时,就无法自动切到 AUTH_USER_MODEL 配置的用户模型 ,因此应该直接引用 AUTH_USER_MODEL 配置的用户模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# mgr.py

from django.db import models
from .base import CommonInfo
from django.conf import settings # 指向 django 项目的配置模块 settings.py


# 项目
class Project(CommonInfo):
PRO_STATUS = (
(0, 'developing'),
(1, 'operating'),
(2, 'stable'),
)
admin = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, related_name='admin_pros', verbose_name='%(app_label)s_%(class)s_admin')

members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='member_pros',verbose_name='%(app_label)s_%(class)s_members')

name = models.CharField('项目名称', max_length=256, unique=True)
status = models.SmallIntegerField('项目状态', choices=PRO_STATUS, default=0)
version = models.CharField('项目版本', default='v0.1', max_length=10)

class Meta(CommonInfo.Meta): # 需要显示继承元类,才会继承相关属性(abstract 除外)
verbose_name = '项目表'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# base.py

from django.db import models
from django.conf import settings


class CommonInfo(models.Model):
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, verbose_name='创建者',
related_name='%(app_label)s_%(class)s_create')
updater = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, null=True, verbose_name='更新者',
related_name='%(app_label)s_%(class)s_update')

create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间', null=True)
decs = models.CharField('描述', max_length=1024, null=True, blank=True)

class Meta:
abstract = True # 定义为抽象模型,不会创建数据库
ordering = ['id']

同步数据库:

1
2
python manage.py makemigrations sqpt
python manage.py migrate

四、 视图开发

1. 创建序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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__'

2. 创建视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# views.py

# 项目
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_lest(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_obj = User.objects.get(pk=_id)
except User.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) # 如果 ID 不正确则返回异常
serializer = UserSerializer(user_obj) # 通过用户数据对象构造序列化器
return Response(serializer.data)

3. 创建路由

1
2
3
4
5
6
7
8
# urls.py

urlpatterns = [
# 引用相关路由
path('', include(router.urls)),
path('users/', views.user_lest),
path('users/<int:_id>', views.user_detail),
]