from multiprocessing.spawn import old_main_modules from ninja_extra import api_controller, ControllerBase, route from ninja import Query from ninja_jwt.authentication import JWTAuth from ninja_extra.permissions import IsAuthenticated from ninja.pagination import paginate from ninja.errors import HttpError from utils.chen_pagination import MyPagination from django.db import transaction from django.db.models.functions import Replace from django.db.models import Q, F, Value from django.shortcuts import get_object_or_404 from typing import List from utils.chen_response import ChenResponse from utils.chen_crud import multi_delete_testDemand from utils.codes import HTTP_INDEX_ERROR from apps.project.models import Design, Dut, Round, TestDemand, TestDemandContent, TestDemandContentStep from apps.project.schemas.testDemand import DeleteSchema, TestDemandModelOutSchema, TestDemandFilterSchema, \ TestDemandTreeReturnSchema, TestDemandTreeInputSchema, TestDemandCreateOutSchema, \ TestDemandCreateInputSchema, ReplaceDemandContentSchema, PriorityReplaceSchema, \ TestDemandRelatedSchema, TestDemandExistRelatedSchema, DemandCopyToDesignSchema, \ TestDemandMultiCreateInputSchema # 导入ORM from apps.project.models import Project # 导入工具 from apps.project.tools.copyDemand import demand_copy_to_design from apps.project.tools.delete_change_key import demand_delete_sub_node_key from utils.smallTools.interfaceTools import conditionNoneToBlank from apps.project.tool.batchTools import parse_test_content_string @api_controller("/project", auth=JWTAuth(), permissions=[IsAuthenticated], tags=['测试项接口']) class TestDemandController(ControllerBase): @route.get("/getTestDemandList", response=List[TestDemandModelOutSchema], exclude_none=True, url_name="testDemand-list") @transaction.atomic @paginate(MyPagination) def get_test_demand_list(self, datafilter: TestDemandFilterSchema = Query(...)): conditionNoneToBlank(datafilter) query_params = { 'project__id': datafilter.project_id, 'ident__icontains': datafilter.ident, 'name__icontains': datafilter.name, 'testType__contains': datafilter.testType, 'priority__icontains': datafilter.priority } # 如果没有传递多个key则认为是“那个轮次汇总界面” if datafilter.dut_id and datafilter.design_id: design_key = "".join([datafilter.round_id, '-', datafilter.dut_id, '-', datafilter.design_id]) query_params['design__key'] = design_key else: # 轮次汇总界面要查round__key query_params['round__key'] = datafilter.round_id # 判断是否存在testDesciption有则表示是大表查询 if datafilter.testDesciption: query_params['testDesciption__icontains'] = datafilter.testDesciption qs = TestDemand.objects.filter(**query_params).order_by("key") # 判断是否存在testContent有则表示是大表查询,这里需要查询子字段 if datafilter.testContent: qs = qs.filter(Q(testQField__subName__icontains=datafilter.testContent) | Q(testQField__testStepField__operation__icontains=datafilter.testContent) | Q(testQField__testStepField__expect__icontains=datafilter.testContent)) # 由于有嵌套query_set存在,把每个测试需求的schema加上一个字段 query_list = [] for query_single in qs: # 遍历每一个测试子项 sub_list = [] for step_obj in query_single.testQField.all(): setattr(step_obj, "subStep", step_obj.testStepField.all().values()) sub_list.append(step_obj) setattr(query_single, "testContent", sub_list) query_list.append(query_single) return query_list @route.get("/getTestDemandOne", response=TestDemandModelOutSchema, url_name='testDemand-one') @transaction.atomic def get_test_demand_one(self, project_id: int, key: str): demand_qs = TestDemand.objects.filter(project_id=project_id, key=key).first() if demand_qs: sub_list = [] for step_obj in demand_qs.testQField.all(): setattr(step_obj, "subStep", step_obj.testStepField.all().values()) sub_list.append(step_obj) setattr(demand_qs, "testContent", sub_list) return demand_qs raise HttpError(500, "未找到相应的数据") # 根据id直接查询 @route.get("/getTestDemandOneById", response=TestDemandModelOutSchema, url_name='testDemand-one-by-id') @transaction.atomic def get_demand_by_id(self, id: int): demand_qs = TestDemand.objects.filter(id=id).first() if demand_qs: sub_list = [] for step_obj in demand_qs.testQField.all(): setattr(step_obj, "subStep", step_obj.testStepField.all().values()) sub_list.append(step_obj) setattr(demand_qs, "testContent", sub_list) return demand_qs raise HttpError(500, "未找到相应的数据") # 处理树状数据 @route.get("/getTestdemandInfo", response=List[TestDemandTreeReturnSchema], url_name="testDemand-info") @transaction.atomic def get_testDemand_tree(self, payload: TestDemandTreeInputSchema = Query(...)): qs = TestDemand.objects.filter(project__id=payload.project_id, design__key=payload.key) return qs # 添加测试项 @route.post("/testDemand/save", response=TestDemandCreateOutSchema, url_name="testDemand-create") @transaction.atomic def create_test_demand(self, payload: TestDemandCreateInputSchema): asert_dict = payload.dict(exclude_none=True) # ident判重 project_qs = Project.objects.filter(id=payload.project_id).first() if payload.ident and project_qs: old_obj = project_qs.ptField.filter(ident=payload.ident).first() # 2025/06/24修改,现在运行不同测试类型有相同的标识 if old_obj and old_obj.testType == payload.testType: return ChenResponse(code=500, status=500, message='测试项标识和其他测试项重复,请更换测试项标识!!!') # 构造design_key design_key = "".join([payload.round_key, "-", payload.dut_key, '-', payload.design_key]) # 查询当前key应该为多少 test_demand_count = TestDemand.objects.filter(project__id=payload.project_id, design__key=design_key).count() key_string = ''.join([design_key, "-", str(test_demand_count)]) # 查询当前各个前面节点的instance round_instance = Round.objects.get(project__id=payload.project_id, key=payload.round_key) dut_instance = Dut.objects.get(project__id=payload.project_id, key="".join([payload.round_key, "-", payload.dut_key])) design_instance = Design.objects.get(project__id=payload.project_id, key="".join( [payload.round_key, "-", payload.dut_key, '-', payload.design_key])) asert_dict.update( {'key': key_string, 'round': round_instance, 'dut': dut_instance, 'design': design_instance, 'title': payload.name}) asert_dict.pop("round_key") asert_dict.pop("dut_key") asert_dict.pop("design_key") asert_dict.pop("testContent") # 创建测试项 - 以及子项/子项步骤 qs = TestDemand.objects.create(**asert_dict) for item in payload.dict()['testContent']: content_obj = TestDemandContent.objects.create( testDemand=qs, subName=item['subName'], subDescription=item['subDescription'] ) TestDemandContentStep.objects.bulk_create([ TestDemandContentStep( testDemandContent=content_obj, **step.dict() if not isinstance(step, dict) else step ) for step in item['subStep'] ]) return qs # 批量新增测试项 @route.post("/testDemand/multi_save", url_name="testDemand-multi-create") @transaction.atomic def create_multi_test_demand(self, payload: TestDemandMultiCreateInputSchema): # 1.首先判断测试项标识是否重复 project_qs = Project.objects.filter(id=payload.project_id).first() designs = project_qs.psField.all() ## 给返回response的data数据以便前端更新树状目录 keys = [] ## 遍历payload.demands数组 for index, demandOne in enumerate(payload.demands): if demandOne.ident and project_qs: old_obj = project_qs.ptField.filter(ident=demandOne.ident).first() if old_obj and old_obj.testType == demandOne.testType: message_temp = f"第{index}个测试项标识重复,请修改" return ChenResponse(status=200, code=500101, data=index, message=message_temp) # 标识不重复就开始录入了 for index, demand in enumerate(payload.demands): create_sub_demands = parse_test_content_string(demand.testContent) if isinstance(create_sub_demands, ChenResponse): return create_sub_demands else: # 这说明解析成功了 # 首先查询所属design、dut、round,方便新增 design_obj: Design = designs.filter(key=demand.parent_key).first() # 因为前端限制必然有 dut_obj = design_obj.dut round_obj = design_obj.round test_demand_count = TestDemand.objects.filter(project=project_qs, design=design_obj).count() key_string = ''.join([design_obj.key, "-", str(test_demand_count)]) keys.append(key_string) create_demand_dict = { 'ident': demand.ident, 'name': demand.name, 'adequacy': demand.adequacy, 'priority': demand.priority, 'testType': demand.testType, 'testMethod': demand.testMethod, 'title': demand.name, 'key': key_string, 'project': project_qs, 'round': round_obj, 'dut': dut_obj, 'design': design_obj, 'testDesciption': demand.testDesciption } demand_created = TestDemand.objects.create(**create_demand_dict) # 录入测试子项 for sub in create_sub_demands: content_obj = TestDemandContent.objects.create( testDemand=demand_created, subName=sub['subName'], subDescription=sub['subDescription'] ) TestDemandContentStep.objects.bulk_create([ TestDemandContentStep( testDemandContent=content_obj, **step.dict() if not isinstance(step, dict) else step ) for step in sub['subStep'] ]) return ChenResponse(code=200991, status=200, data=keys, message='成功录入') # 更新测试项 @route.put("/testDemand/update/{id}", response=TestDemandCreateOutSchema, url_name="testDemand-update") @transaction.atomic def update_testDemand(self, id: int, payload: TestDemandCreateInputSchema): project_qs = get_object_or_404(Project, id=payload.project_id) # 查到当前 testDemand_qs = TestDemand.objects.get(id=id) old_ident = testDemand_qs.ident # 用于判断是否要集体修改case的ident for attr, value in payload.dict().items(): # 判重复 if attr == 'ident': # 先判断是否和原标识一样,且测试类型改变 if payload.dict()['testType'] != testDemand_qs.testType and value == old_ident: old_obj = project_qs.ptField.filter(ident=payload.ident).first() # 2025/06/24修改不同类型可以相同 if old_obj and old_obj.testType == payload.dict()['testType']: return ChenResponse(code=500, status=500, message='更换的标识和其他测试项重复') if attr == 'project_id' or attr == 'round_key' or attr == 'dut_key' or attr == 'design_key': continue # 如果发现是key则不处理 if attr == 'name': setattr(testDemand_qs, "title", value) # 找到attr为testContent的 if attr == 'testContent': content_list = testDemand_qs.testQField.all() for content_single in content_list: # 删除TestDemandContent,会把CASCADE的TestDemandContentStep也删除 content_single.delete() # 添加测试项步骤 for item in value: # 遍历的是testContent字段,所以每个item是TestDemandContent的数据 # 存在subName就添加一个测试子项 if item['subName']: content_obj = TestDemandContent.objects.create( testDemand=testDemand_qs, subName=item["subName"], subDescription=item["subDescription"] ) TestDemandContentStep.objects.bulk_create([ TestDemandContentStep( testDemandContent=content_obj, **step.dict() if not isinstance(step, dict) else step ) for step in item["subStep"] ]) setattr(testDemand_qs, attr, value) # ~~~2024年5月9日:测试项更新标识后还要更新下面用例的标识~~~ if testDemand_qs.ident != old_ident: for case in testDemand_qs.tcField.all(): case.ident = testDemand_qs.ident case.save() testDemand_qs.save() return testDemand_qs # 删除测试项 @route.delete("/testDemand/delete", url_name="testDemand-delete") @transaction.atomic def delete_testDemand(self, data: DeleteSchema): # 根据其中一个id查询出dut_id try: test_demand_single = TestDemand.objects.filter(id=data.ids[0])[0] except IndexError: return ChenResponse(status=500, code=HTTP_INDEX_ERROR, message='您未选择需要删除的内容') design_id = test_demand_single.design.id design_key = test_demand_single.design.key multi_delete_testDemand(data.ids, TestDemand) index = 0 test_demand_all_qs = TestDemand.objects.filter(design__id=design_id).order_by('id') for single_qs in test_demand_all_qs: test_demand_key = "".join([design_key, '-', str(index)]) single_qs.key = test_demand_key index = index + 1 single_qs.save() demand_delete_sub_node_key(single_qs) # 删除后需重排子节点 return ChenResponse(message="测试需求删除成功!") # 查询一个项目的所有测试项【当前轮次】 @route.get("/testDemand/getRelatedTestDemand", url_name="testDemand-getRelatedTestDemand") @transaction.atomic def getRelatedTestDemand(self, id: int, round: str): project_qs = get_object_or_404(Project, id=id) # 找出属于该轮次的所有测试项 round_qs = project_qs.pField.filter(key=round).first() designs = round_qs.dsField.all() data_list = [] for design in designs: design_dict = {'label': design.name, 'value': design.id, 'children': []} for test_item in design.dtField.all(): test_item_dict = {'label': test_item.name, 'value': test_item.id, 'key': test_item.key} design_dict['children'].append(test_item_dict) data_list.append(design_dict) return ChenResponse(message='获取成功', data=data_list) # 处理desgin关联testDemand接口 @route.post('/testDemand/solveRelatedTestDemand', url_name="testDemand-solveRelatedTestDemand") @transaction.atomic def solveRelatedTestDemand(self, data: TestDemandRelatedSchema): test_item_ids = data.data non_exist_ids = [x for x in test_item_ids] project_qs = get_object_or_404(Project, id=data.project_id) key_str = "-".join([data.round_key, data.dut_key, data.design_key]) design_item = project_qs.psField.filter(key=key_str).first() if design_item: # 将test_item_ids中本身具有的测试项从id数组中移除 for test_id in test_item_ids: for ti in design_item.dtField.all(): if ti.pk == test_id: non_exist_ids.remove(test_id) if len(non_exist_ids) <= 0 < len(test_item_ids): return ChenResponse(status=400, code=200, message='选择的测试项全部存在于当前设计需求中,请重新选择...') # 先查询现在有的关联测试项 for item in design_item.odField.values('id'): item_id = item.get('id', None) if not item_id in test_item_ids: test_item_obj = TestDemand.objects.filter(id=item_id).first() design_item.odField.remove(test_item_obj) for test_item_id in non_exist_ids: test_items = design_item.odField.filter(id=test_item_id) if len(test_items) <= 0: # 查询testDemand design_item.odField.add(TestDemand.objects.filter(id=test_item_id).first()) return ChenResponse(status=200, code=200, message='添加关联测试项成功...') else: return ChenResponse(status=400, code=400, message='设计需求不存在,请检查...') # 找出已关联的测试项给前端的cascader @route.post('/testDemand/getExistRelatedTestDemand', url_name="testDemand-getExistRelatedTestDemand") @transaction.atomic def getExistRelatedTestDemand(self, data: TestDemandExistRelatedSchema): project_qs = get_object_or_404(Project, id=data.project_id) key_str = "-".join([data.round_key, data.dut_key, data.design_key]) design_item = project_qs.psField.filter(key=key_str).first() ids = [] if design_item: for item in design_item.odField.all(): ids.append(item.id) return ids # 前端测试项右键复制到某个设计需求下面 @route.post('/testDemand/copy_to_design', url_name='testDemand-copy') @transaction.atomic def copy_to_design(self, data: DemandCopyToDesignSchema): """前端测试项右键复制到某个设计需求下面""" new_demand_key = demand_copy_to_design(data.project_id, data.demand_key, data.design_id, data.depth) return ChenResponse(data={'key': new_demand_key}) # 测试项-替换接口 @route.post("/testDemand/replace/", url_name='testDemand-replace') @transaction.atomic def replace_demand_content(self, payload: ReplaceDemandContentSchema): # 1.首先查询项目 project_obj: Project = get_object_or_404(Project, id=payload.project_id) # 2.查询[所有轮次]的selectRows的id demand_qs = project_obj.ptField.filter(id__in=payload.selectRows, round__key=payload.round_key) # 3.批量替换里面文本(解构不影响老数组) selectColumn = [x for x in payload.selectColumn if x != 'testContent'] replace_kwargs = { field_name: Replace(F(field_name), Value(payload.originText), Value(payload.replaceText)) for field_name in selectColumn } # 4.单独处理testContentStep的操作、预期-查询所有 # 4.1.获取所有关联的TestDemandContentStep step_count = 0 if 'testContent' in payload.selectColumn: test_demand_contents = TestDemandContent.objects.filter(testDemand__in=demand_qs) test_steps = TestDemandContentStep.objects.filter(testDemandContent__in=test_demand_contents) # 批量更新 operation 和 expect step_count = test_steps.update( operation=Replace(F('operation'), Value(payload.originText), Value(payload.replaceText)), expect=Replace(F('expect'), Value(payload.originText), Value(payload.replaceText)) ) # 5.提交更新 replace_count = demand_qs.update(**replace_kwargs) return {'count': replace_count + step_count} # 批量替换优先级-priority @route.post("/testDemand/priorityReplace/", url_name='demand-priority-replace') @transaction.atomic def multiple_modify_demand_priority(self, payload: PriorityReplaceSchema): # 替换优先级 demand_qs = TestDemand.objects.filter(id__in=payload.selectRows) demand_qs.update(priority=payload.priority)