测试平台之reports应用的前世今生,以及django_filters的不同类型的字段如何使用,达到前端过滤的效果

### 总路由 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 查询条件,代码更简洁、易维护。
                
    """


### 














posted @ 2026-02-02 15:59  大海一个人听  阅读(33)  评论(0)    收藏  举报