### 总路由 lemontest/urls.py
"""lemontest URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include, re_path
from rest_framework.documentation import include_docs_urls
from drf_yasg.views import get_schema_view as yasg_get_schema_view
from drf_yasg import openapi
# from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework.permissions import IsAuthenticated
schema_view = yasg_get_schema_view(
openapi.Info(
title='你的项目名称',
default_version="1.0.0",
description='项目描述',
terms_of_service='http://127.0.0.1:8000/',
contact=openapi.Contact(email='[email protected]'),
license=openapi.License(name='MIT')
),
public=True,
# permission_classes=(IsAuthenticated, ), # 权限
)
urlpatterns = [
path('admin/', admin.site.urls),
# 这里面实现了用户的登录退出功能,可以查看rest_framework.urls中的内容
path('restframework/', include('rest_framework.urls')),
path('', include('users.urls')),
path('', include('projects.urls')),
path('', include('testplans.urls')),
path('', include('reports.urls')),
path('', include('bugs.urls')),
path('docs/', include_docs_urls(title="你的项目名称", public=True, description='描述信息')),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(), name='swagger-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger'), name='swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc'), name='redoc-ui'),
]
### reports/urls.py
from django.urls import path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register('record', views.RecordViewSet)
urlpatterns: list = router.urls
### models.py
from django.db import models
class Record(models.Model):
"""运行记录表"""
create_time = models.DateTimeField(verbose_name='创建时间', help_text='创建时间', auto_now_add=True)
plan = models.ForeignKey('testplans.TestPlan', help_text='执行计划', verbose_name='执行计划', on_delete=models.PROTECT, related_name='records')
all = models.IntegerField(help_text='用例总数', verbose_name='用例总数',blank=True,default=0)
success = models.IntegerField(help_text='成功用例', verbose_name='成功用例',blank=True,default=0)
fail = models.IntegerField(help_text='失败用例', verbose_name='失败用例',blank=True,default=0)
error = models.IntegerField(help_text='错误用例', verbose_name='错误用例',blank=True,default=0)
pass_rate = models.CharField(help_text='执行通过率', verbose_name='执行通过率', max_length=100, blank=True,default=0)
tester = models.CharField(help_text='执行者', verbose_name='执行者', max_length=100, blank=True)
test_env = models.ForeignKey('projects.TestEnv', help_text='测试环境', verbose_name='测试环境', on_delete=models.PROTECT)
statue = models.CharField(help_text='执行状态', verbose_name='执行状态', max_length=100) # 之前的代码
# status = models.CharField(help_text='执行状态', verbose_name='执行状态', max_length=100) # 我自己修改后的代码,把statue改成了status,未迁移
def __str__(self):
return str(self.id)
class Meta:
db_table = 'tb_record'
verbose_name = '运行记录表'
verbose_name_plural = verbose_name
class Report(models.Model):
"""测试报告"""
info = models.JSONField(help_text='测试报告', verbose_name='测试报告', default=dict, blank=True)
record = models.OneToOneField('Record', help_text='测试记录', verbose_name='测试记录', on_delete=models.PROTECT)
# 数据库中存储该字段名称叫record_id;
# 关联字段在哪一方设置,哪一方就是正向关联的出发点。
def __str__(self):
return str(self.id)
class Meta:
db_table = 'tb_report'
verbose_name = '报告表'
verbose_name_plural = verbose_name
# Report.objects.create(info=res, record_id=record_id)中record_id=record_id的核心作用是指定 Report 关联的 Record 主键
"""
关联字段在哪一方设置,哪一方就是正向关联的出发点。
正向关联,反向关联,理解双向查询:
操作: 本质: 对应的SQL:
report.record【正向】 通过Report的record_id找Record select * from tb_record where id=report.record_id;
record.report【反向】 通过Record的id找Report select * from tb_report where record_id=record.id;
"""
### serializers.py
from rest_framework import serializers
from . import models
class RecordSerializer(serializers.ModelSerializer):
plan_name = serializers.StringRelatedField(source='plan')
env_name = serializers.StringRelatedField(source='test_env')
class Meta:
model = models.Record
fields = '__all__'
class ReportSerializer(serializers.ModelSerializer):
class Meta:
model = models.Report
fields = '__all__'
### views.py
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import IsAuthenticated
from . import models
from . import serializers
from . import filters
class RecordViewSet(ReadOnlyModelViewSet):
queryset = models.Record.objects.all()
serializer_class = serializers.RecordSerializer
permission_classes = [IsAuthenticated]
filterset_class = filters.RecordFilterSet
@action(methods=['get'], detail=True)
def report(self, request: Request, *args, **kwargs):
# 获取测试记录对象
record_obj = self.get_object()
report_obj = record_obj.report
# 通过测试记录对象获取测试报告,并序列化输出
serializer = serializers.ReportSerializer(instance=report_obj)
return Response(serializer.data)
### filters.py
from django_filters import rest_framework as filters
from . import models
class RecordFilterSet(filters.FilterSet):
# field_name有点类似于Serializer字段中source属性
# 指定orm模型中查询类型
# Record.objects.filter(plan__project=1)
project = filters.NumberFilter(field_name='plan__project')
env = filters.NumberFilter(field_name='test_env')
class Meta:
model = models.Record # 指定要过滤的模型
fields = ['project', 'env', 'plan']
"""
你核心想弄明白的是 filterset_class = filters.RecordFilterSet 的作用,以及 RecordFilterSet 类中字段定义、fields 属性的具体含义和用法,这其实是 Django Filter 结合 DRF 实现灵活的列表数据过滤的核心逻辑。
一、filterset_class = filters.RecordFilterSet 的核心作用
在 DRF 的 ModelViewSet(包括你用的 ReadOnlyModelViewSet)中,filterset_class 是用来指定当前视图集的过滤规则类,它的核心作用是:
让前端可以通过 URL 参数(如 ?project=1&env=2&plan=3)对 Record 列表数据进行精准过滤;
将前端传入
的过滤参数,自动转换为 Django ORM 的 filter() 查询条件,无需你手动写 if...else 判断参数、拼接查询语句;
结合 DRF 的过滤框架,实现安全、可复用、易扩展的过滤逻辑。
简单说:没有这个配置时,访问 /records/ 只能返回所有 Record 数据;配置后,访问 /records/?project=1&env=2 就能只返回「属于项目 1、环境 2」的测试记录。
二、RecordFilterSet 类的完整解析
RecordFilterSet 继承自 filters.FilterSet(Django Filter 提供的过滤基类),是用来定义过滤规则的核心类,下面拆解它的每一部分:
1. 类内自定义过滤字段的定义(project/env)
project = filters.NumberFilter(field_name='plan__project')
env = filters.NumberFilter(field_name='test_env')
这两行是定义自定义过滤字段,我们先拆解关键概念:
(1)filters.NumberFilter:过滤字段的类型
Django Filter 提供了多种过滤字段类型,对应不同的数据类型和过滤逻辑,常用的有:
NumberFilter:用于过滤数字类型的字段(如主键 ID、整数),会自动校验参数是否为数字,避免类型错误;
CharFilter:用于过滤字符串类型字段;
BooleanFilter:用于过滤布尔类型字段;
DateTimeFilter:用于过滤时间类型字段(还支持 gte/lte 等范围过滤)。
你这里的 project 和 env 都是关联表的主键(数字),所以用 NumberFilter 最合适。
(2)field_name 参数:指定「ORM 关联路径」
field_name 是过滤规则的核心,它的作用是把前端传入的过滤参数名(如 project),映射到 Django ORM 模型的字段路径。
例子 1:project = filters.NumberFilter(field_name='plan__project')
前端传参:?project=1
ORM 转换:Record.objects.filter(plan__project=1)
含义拆解:
plan:Record 模型中的外键字段(关联 TestPlan);
__(双下划线):Django ORM 的「跨表查询符」,表示「通过 plan 字段关联到 TestPlan 表」;
project:TestPlan 模型中的字段(应该是关联 Project 的外键);
整体:过滤出「Record 的 plan 关联的 TestPlan 的 project 字段等于 1」的记录。
例子 2:env = filters.NumberFilter(field_name='test_env')
前端传参:?env=2
ORM 转换:Record.objects.filter(test_env=2)
含义:过滤出「Record 的 test_env 字段(外键)等于 2」的记录(因为 test_env 是 Record 直接的外键,无需跨表,所以 field_name 直接写字段名)。
(3)自定义过滤字段的核心价值
如果没有自定义 project 这个过滤字段,前端无法直接通过 ?project=1 过滤(因为 Record 模型本身没有 project 字段);通过 field_name='plan__project',我们实现了「前端传简单参数名,后端自动跨表过滤」的效果,无需手动写跨表查询逻辑。
2. Meta 类中的 fields 属性
class Meta:
model = models.Record # 指定要过滤的模型
fields = ['project', 'env', 'plan']
fields 的核心作用是:声明当前 FilterSet 允许使用的所有过滤字段(包括自定义字段 + 模型原生字段),具体拆解:
(1)model = models.Record
指定这个 FilterSet 是为 Record 模型服务的,Django Filter 会基于这个模型的字段结构,校验过滤规则的合法性(比如 field_name 对应的路径是否存在)。
(2)fields = ['project', 'env', 'plan']
包含的内容:
project:你自定义的过滤字段(对应 field_name='plan__project');
env:你自定义的过滤字段(对应 field_name='test_env');
plan:Record 模型的原生外键字段(无需自定义,Django Filter 会自动生成默认的过滤规则)。
核心作用:
白名单控制:只有 fields 中列出的字段,才允许前端作为过滤参数传入(比如前端传 ?tester=张三 会被忽略,因为 tester 不在 fields 中),避免恶意参数;
自动生成默认过滤规则:对于 fields 中属于模型原生字段的部分(如 plan),无需手动定义,Django Filter 会自动生成对应的过滤规则(比如 plan 是外键,自动生成 NumberFilter,前端传 ?plan=5 就能过滤出 plan_id=5 的记录);
统一管理过滤字段:把所有允许的过滤字段集中声明,便于维护。
三、完整的执行流程(以前端请求为例)
假设前端发送请求:GET /records/?project=1&env=2&plan=3
DRF 接收到请求后,发现视图集配置了 filterset_class = RecordFilterSet;
Django Filter 解析 URL 参数:project=1、env=2、plan=3;
匹配过滤规则:
project=1 → 匹配 project = NumberFilter(field_name='plan__project') → 转换为 ORM 条件:plan__project=1;
env=2 → 匹配 env = NumberFilter(field_name='test_env') → 转换为 ORM 条件:test_env=2;
plan=3 → 自动匹配 Record 模型的 plan 字段 → 转换为 ORM 条件:plan=3;
拼接最终的 ORM 查询:
Record.objects.filter(plan__project=1, test_env=2, plan=3)
执行查询,将结果序列化后返回给前端。
四、 补充:如果没有 fields 会怎样?
如果省略 Meta 中的 fields,或者写成 fields = '__all__':
fields='__all__':允许前端对 Record 模型的所有原生字段进行过滤,但你自定义的 project/env 字段会失效;
省略 fields:Django Filter 会抛出异常,因为必须明确声明允许的过滤字段(安全设计)。
所以你的写法(自定义字段 + fields 声明)是最佳实践:既支持跨表的自定义过滤,又严格控制允许的过滤参数。
总结:
1.filterset_class = filters.RecordFilterSet:为 RecordViewSet 指定过滤规则类,让前端能通过 URL 参数过滤 Record 列表数据;
2.RecordFilterSet 中的自定义字段(project/env):通过 field_name 定义「前端参数名 → ORM 关联路径」的映射,实现跨表 / 精准过滤,NumberFilter 限定参数类型为数字;
3.Meta 中的 fields:声明允许的过滤字段白名单,自动为模型原生字段生成过滤规则,同时控制前端可传入的过滤参数范围。
核心价值:用配置化的方式替代手动拼接过滤条件,让过滤逻辑更清晰、可复用、易维护,同时保证参数安全。
### 除了`NumberFilter`,Django Filter 还提供了哪些常用的过滤字段类型?
除了 NumberFilter 之外,Django Filter 提供的其他常用过滤字段类型,这是掌握 Django 过滤功能的关键 —— 不同的字段类型对应不同的数据类型和过滤场景,能帮你更精准地实现各种过滤需求。
一、Django Filter 常用过滤字段类型(按场景分类)
Django Filter 提供的过滤字段类型本质上是对「数据类型 + 过滤逻辑」的封装,下面按常用程度和使用场景分类讲解,每个类型都包含用途、示例和适用场景,方便你直接套用:
1. 基础数据类型过滤(最常用)
这类过滤字段对应 Django 模型的基础字段类型,仅做「等于」过滤,是最基础的用法:
过滤字段类型 | 用途 | 示例(FilterSet 中定义) | 前端传参示例
CharFilter | 过滤字符串类型字段(CharField、TextField) | name = filters.CharFilter(field_name='name') | ?name=测试计划1
BooleanFilter | 过滤布尔类型字段(BooleanField) | is_active = filters.BooleanFilter(field_name='is_active') | ?is_active=True
DateFilter | 过滤日期类型字段(DateField) | create_date = filters.DateFilter(field_name='create_time') | ?create_date=2026-01-28
DateTimeFilter | 过滤日期时间类型字段(DateTimeField) | create_time = filters.DateTimeFilter(field_name='create_time') | ?create_time=2026-01-28 10:00:00
TimeFilter | 过滤时间类型字段(TimeField) | start_time = filters.TimeFilter(field_name='start_time') | ?start_time=10:00:00
2. 范围 / 比较类过滤(高频使用)
这类过滤字段用于实现「大于、小于、包含、模糊匹配」等复杂逻辑,是业务中最常用的进阶用法:
过滤字段类型 | 用途 | 示例(FilterSet 中定义) | 前端传参示例
NumberFilter(补充)| 数字类型「等于」过滤(IntegerField、FloatField、ForeignKey 主键)| plan = filters.NumberFilter(field_name='plan') | ?plan=5
RangeFilter | 数字 / 日期「范围」过滤(大于等于最小值 + 小于等于最大值)| id_range = filters.RangeFilter(field_name='id') | ?id_range_min=1&id_range_max=10
DateRangeFilter| 日期「范围」过滤(专用,更友好) | create_date_range = filters.DateRangeFilter(field_name='create_time')| ?create_date_range_after=2026-01-01&create_date_range_before=2026-01-31
ContainsFilter | 字符串「包含」过滤(模糊匹配,区分大小写) | name_contains = filters.ContainsFilter(field_name='name') | ?name_contains=测试
IContainsFilter | 字符串「包含」过滤(不区分大小写) | name_icontains = filters.IContainsFilter(field_name='name') | ?name_icontains=TEST
StartsWithFilter | 字符串「开头匹配」(区分大小写)| name_start = filters.StartsWithFilter(field_name='name') | ?name_start=测试
IStartsWithFilter | 字符串「开头匹配」(不区分大小写)| name_istart = filters.IStartsWithFilter(field_name='name') | ?name_istart=TEST
EndsWithFilter | 字符串「结尾匹配」(区分大小写)| name_end = filters.EndsWithFilter(field_name='name') | ?name_end=计划1
IEndsWithFilter | 字符串「结尾匹配」(不区分大小写)| name_iend = filters.IEndsWithFilter(field_name='name') | ?name_iend=PLAN1
GreaterThanFilter | 数字 / 日期「大于」过滤 | id_gt = filters.GreaterThanFilter(field_name='id') | ?id_gt=5
GreaterThanOrEqualFilter | 数字 / 日期「大于等于」过滤 | id_gte = filters.GreaterThanOrEqualFilter(field_name='id') | ?id_gte=5
LessThanFilter | 数字 / 日期「小于」过滤 | id_lt = filters.LessThanFilter(field_name='id') | ?id_lt=10
LessThanOrEqualFilter | 数字 / 日期「小于等于」过滤 | id_lte = filters.LessThanOrEqualFilter(field_name='id') | ?id_lte=10
3. 特殊场景过滤
这类过滤字段针对特殊需求,比如空值、in 条件等:
过滤字段类型 | 用途 | 示例(FilterSet 中定义) | 前端传参示例
IsNullFilter | 过滤「字段为空 / 不为空」的记录 | desc_isnull = filters.IsNullFilter(field_name='desc') | ?desc_isnull=True(为空)/False(不为空)
InFilter | 过滤「字段值在指定列表中」的记录(多值匹配) | id_in = filters.InFilter(field_name='id') | ?id_in=1,2,3
ExcludeFilter | 过滤「排除指定值」的记录(反向过滤) | id_exclude = filters.ExcludeFilter(field_name='id') | ?id_exclude=5
二、实战示例:扩展你的 RecordFilterSet
结合你的 Record 模型,我们扩展一个更完整的 FilterSet,演示多种过滤类型的用法:
from django_filters import rest_framework as filters
from . import models
class RecordFilterSet(filters.FilterSet):
# 基础类型(已有的)
project = filters.NumberFilter(field_name='plan__project')
env = filters.NumberFilter(field_name='test_env')
# 范围过滤:ID在1-100之间
id_range = filters.RangeFilter(field_name='id')
# 日期范围过滤:创建时间在指定区间
create_time_range = filters.DateRangeFilter(field_name='create_time')
# 字符串包含过滤:执行者包含「张三」
tester_contains = filters.ContainsFilter(field_name='tester')
# 数字大于过滤:成功用例数大于10
success_gt = filters.GreaterThanFilter(field_name='success')
# 空值过滤:bug描述为空
desc_isnull = filters.IsNullFilter(field_name='desc')
class Meta:
model = models.Record
# fields 包含自定义字段 + 原生字段
fields = ['project', 'env', 'plan', 'id_range', 'create_time_range', 'tester_contains', 'success_gt', 'desc_isnull']
前端可以这样组合过滤:
GET /records/?project=1&env=2&success_gt=10&tester_contains=张三&create_time_range_after=2026-01-01
含义:过滤出「项目 1、环境 2、成功用例数 > 10、执行者包含张三、创建时间在 2026-01-01 之后」的测试记录。
三、关键使用技巧
命名规范:自定义过滤字段名建议清晰(如 success_gt、tester_contains),前端传参时能直观理解含义;
跨表兼容:所有过滤类型都支持 field_name 用 __ 跨表(如 plan__project__name_contains);
默认值 / 必填:可以通过 required=True 强制要求传参,default 设置默认值(如 success_gt = filters.GreaterThanFilter(field_name='success', default=0));
多条件组合:前端可同时传多个过滤参数,Django Filter 会自动拼接为 AND 条件(如 ?success_gt=10&fail_lt=5 表示「成功 > 10 且 失败 < 5」)。
【总结:】
Django Filter 的过滤字段类型按「基础类型」「范围 / 比较」「特殊场景」分类,核心是匹配模型字段类型和业务过滤逻辑;
基础类型(CharFilter/BooleanFilter/DateFilter)用于简单的「等于」过滤,范围 / 比较类型(RangeFilter/IContainsFilter/GreaterThanFilter)用于复杂逻辑;
所有过滤类型都通过 field_name 指定模型字段(支持跨表 __),通过 fields 声明允许前端使用的过滤字段白名单。
掌握这些类型后,你可以轻松实现几乎所有常见的列表过滤需求,无需手动拼接 ORM 查询条件,代码更简洁、易维护。
"""
###