Compare commits

..

23 Commits

Author SHA1 Message Date
66e48d3165 日常修复内容20260422 2026-04-22 18:28:32 +08:00
dffc1d5872 日常修复内容 2026-04-20 18:00:55 +08:00
518f2f43a7 修复说明reuse文件夹图片丢失问题 2026-04-16 17:31:02 +08:00
46768e53c3 更新:setuptools抛弃旧API 2026-02-12 13:45:52 +08:00
0638950286 更新setuptools、docxcompose依赖 2026-02-11 09:20:54 +08:00
74d3d22ffe 修复:测试记录序号、测评报告统计、富文本渲染word字符问题 2026-02-09 15:41:17 +08:00
2f58bdc668 新增:影响域分析-软件更改部分 2026-02-08 15:10:43 +08:00
a76cd8674c 新增:影响域分析web录入 2026-02-07 17:26:34 +08:00
0bee950a52 新增:静态、动态环境内容 2026-02-05 18:27:04 +08:00
a2781c902a 修改:渲染软件接口图 2026-02-04 17:08:46 +08:00
007712c63c 新增:软件接口图接口系列 2026-02-04 11:24:15 +08:00
48a0fad7e4 新增:完全代码创建word表格,设置宽度、字体 2026-02-03 16:50:58 +08:00
4a1881bf32 新增:软件概述model、新增修改软件概述 2026-02-02 17:32:53 +08:00
beb8c2d25b v0.1.1 2026-01-28 16:50:40 +08:00
f755422cb3 更新项目由uv管理 2025-12-24 16:59:57 +08:00
8ba5d4fd23 新增问题单详情界面 2025-12-23 10:36:55 +08:00
3e048ea876 增加批量增加用例、测试项、设计需求功能 2025-12-19 18:08:19 +08:00
f3806687b0 增加测试AI接口 2025-12-04 10:34:14 +08:00
9db8b28f5b [Update#1]支撑AI生成测试项-接口调整 2025-12-02 18:13:30 +08:00
a396a8fcfa 保存修订 2025-11-18 10:52:10 +08:00
e6c593c920 Crud表格批量修改替换 2025-05-28 18:44:25 +08:00
1b2c3ec3d6 添加test.py 2025-05-17 18:05:23 +08:00
322096c069 log change 2025-05-15 18:53:48 +08:00
361 changed files with 9591 additions and 4036 deletions

4
.env
View File

@@ -2,4 +2,6 @@ AUTH_LDAP_SERVER_URI='ldap://dns.paisat.cn:389'
AUTH_LDAP_BIND_DN='CN=Administrator,CN=Users,DC=sstc,DC=ctu'
AUTH_LDAP_BIND_PASSWORD='WXWX2019!!!!!!'
BASE_DN='OU=all,DC=sstc,DC=ctu'
FILTER_STR='(sAMAccountName=%(user)s)'
FILTER_STR='(sAMAccountName=%(user)s)'
AUTH_LDAP_SERVER_URI_IP='ldap://192.168.0.201:389'

View File

@@ -18,7 +18,7 @@
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (cdtestplant_v1)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="uv (cdtestplant_v1)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">

4
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.8 (cdtestplant_v1)" />
<option name="sdkName" value="uv (cdtestplant_v1)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (cdtestplant_v1)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="uv (cdtestplant_v1)" project-jdk-type="Python SDK" />
</project>

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13.11

View File

@@ -3,8 +3,7 @@
## 内外V0.0.1版本
2024年7月3日 - V0.0.1版本首次导入内网并进行数据库迁移和部署
2025年12月22日 - V0.1.1版本导入内网并数据库迁移部署
## 外V0.0.2版本

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,212 +1,216 @@
from copy import deepcopy
from pathlib import Path
from ninja_extra import api_controller, ControllerBase, route
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
from django.db import transaction
from django.db.models import QuerySet
from docxtpl import DocxTemplate
from apps.dict.models import Dict
from utils.chen_response import ChenResponse
from django.shortcuts import get_object_or_404
from typing import Union
from docxtpl import InlineImage
from apps.project.models import Dut, Project, Round
from utils.util import get_list_dict, get_str_dict, get_ident, get_case_ident
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from utils.path_utils import project_path
from apps.createDocument.extensions.util import delete_dir_files
from apps.createDocument.extensions.parse_rich_text import RichParser
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
chinese_round_name: list = ['', '', '', '', '', '', '', '', '', '']
# @api_controller("/generateHSM", tags=['生成回归记录系列文档'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generateHJL", tags=['生成回归记录系列文档'])
class GenerateControllerHJL(ControllerBase):
logger = GenerateLogger('回归测试记录')
# important删除之前的文件
@route.get('/create/deleteHJLDocument', url_name='delete-hjl-document')
def delete_hjl_document(self, id: int):
project_path_str = project_path(id)
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hjl'
delete_dir_files(save_path)
@route.get("/create/basicInformation", url_name="create-basicInformation")
@transaction.atomic
def create_basicInformation(self, id: int):
"""生成回归测试记录的被测软件基本信息"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hjl' / '被测软件基本信息.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
# 第一轮次对象
round1_obj: Union[Round, None] = project_obj.pField.filter(key='0').first()
# 第一轮源代码被测件对象
round1_so_dut: Union[Dut, None] = round1_obj.rdField.filter(type='SO').first()
languages = get_list_dict('language', project_obj.language)
language_list = [item['ident_version'] for item in languages]
# 取非第一轮次
hround_list: QuerySet = project_obj.pField.exclude(key='0')
if len(hround_list) < 1:
# ***Inspect-start***
self.logger.model = '回归测试记录'
self.logger.write_warning_log('当前文档全部片段', f'该项目没有创建轮次')
# ***Inspect-end***
return ChenResponse(code=400, status=400, message='您未创建轮次,请创建完毕后再试')
context = {
'project_name': project_obj.name,
'language': "".join(language_list),
'soft_type': project_obj.get_soft_type_display(),
'security_level': get_str_dict(project_obj.security_level, 'security_level'),
'runtime': get_str_dict(project_obj.runtime, 'runtime'),
'devplant': get_str_dict(project_obj.devplant, 'devplant'),
'recv_date': project_obj.beginTime.strftime("%Y-%m-%d"),
'dev_unit': project_obj.dev_unit,
}
version_info = [{'version': round1_so_dut.version,
'line_count': int(round1_so_dut.total_lines),
'effective_line': int(round1_so_dut.effective_lines)}]
# 循环回归的轮次
for hround in hround_list:
# 每个轮次独立渲染context
context_round = deepcopy(context)
# 取中文名称
cname = chinese_round_name[int(hround.key)] # 输出二、三...
# 取该轮次源代码版本放入版本列表
so_dut: Dut = hround.rdField.filter(type='SO').first()
if not so_dut:
return ChenResponse(code=400, status=400, message=f'您第{cname}轮次中缺少源代码被测件,请添加')
version_info.append(
{'version': so_dut.version, 'line_count': int(so_dut.total_lines),
'effective_line': int(so_dut.effective_lines)})
context_round['version_info'] = version_info
# 开始渲染每个轮次的二级文档
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hjl' / f"{cname}轮被测软件基本信息.docx"
doc.render(context=context_round)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归说明文档基本信息生成完毕')
@route.get("/create/caseinfo", url_name="create-caseinfo")
@transaction.atomic
def create_caseinfo(self, id: int):
"""生成回归测试记录的-{测试用例记录}"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hjl' / '测试用例记录.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
hround_list: QuerySet = project_obj.pField.exclude(key='0')
if len(hround_list) < 1:
return None
demand_prefix = '3.1'
# 循环每轮轮次对象
for hround in hround_list:
cname = chinese_round_name[int(hround.key)] # var输出二、三字样
test_type_len = Dict.objects.get(code='testType').dictItem.count() # 测试类型的个数
type_number_list = [i for i in range(1, test_type_len + 1)] # 测试类型编号对应的列表
list_list = [[] for j in range(1, test_type_len + 1)] # 每个测试类型组合为一个列表[[],[],[],[]]
testType_list, last_chapter_items = create_csx_chapter_dict(hround)
testDemands = hround.rtField.all() # 本轮所有测试项
for demand in testDemands:
type_index = type_number_list.index(int(demand.testType))
demand_ident = get_ident(demand)
# ~~~组装测试项~~~
demand_last_chapter = last_chapter_items[demand.testType].index(demand.key) + 1
demand_chapter = ".".join([demand_prefix, str(testType_list.index(demand.testType) + 1),
str(demand_last_chapter)])
demand_dict = {
'name': demand.name,
'ident': demand_ident,
'chapter': demand_chapter,
'item': []
}
# ~~~这里组装测试项里面的测试用例~~~
for case in demand.tcField.all():
step_list = []
index = 1
for one in case.step.all():
# 这里需要对operation富文本处理
rich_parser = RichParser(one.operation)
desc_list = rich_parser.get_final_list(doc, img_size=68)
rich_parser2 = RichParser(one.result)
res_list = rich_parser2.get_final_list(doc, img_size=75)
# 组装用例里面的步骤dict
passed = '通过'
if one.passed == '2':
passed = '未通过'
if one.passed == '3':
passed = '未执行'
step_dict = {
'index': index,
'operation': desc_list,
'expect': one.expect,
'result': res_list,
'passed': passed,
}
step_list.append(step_dict)
index += 1
# 查询所有的problem
problem_list = []
problem_prefix = "PT"
proj_ident = project_obj.ident
for problem in case.caseField.all():
problem_list.append("_".join([problem_prefix, proj_ident, problem.ident]))
# fpga的时序图
rich_parser3 = RichParser(case.timing_diagram)
timing_diagram = rich_parser3.get_final_list(doc, img_size=115, height=50)
has_timing_diagram = False
if len(timing_diagram) > 0:
if isinstance(timing_diagram[0], InlineImage):
has_timing_diagram = True
# 组装用例的dict
case_dict = {
'name': case.name,
'ident': get_case_ident(demand_ident, case),
'summary': case.summarize,
'initialization': case.initialization,
'premise': case.premise,
'design_person': case.designPerson,
'test_person': case.testPerson,
'monitor_person': case.monitorPerson,
'step': step_list,
'time': str(case.exe_time) if case.exe_time is not None else str(case.update_datetime),
'problems': "".join(problem_list),
'round_num_chn': cname,
# 2025年4月24日新增
'has_timing_diagram': has_timing_diagram,
'timing_diagram': timing_diagram,
}
demand_dict['item'].append(case_dict)
list_list[type_index].append(demand_dict)
# 定义渲染上下文
context = {}
output_list = []
for (index, li) in enumerate(list_list):
qs = Dict.objects.get(code="testType").dictItem.get(key=str(index + 1))
context_str = qs.title
sort = qs.sort
table = {
"type": context_str,
"item": li,
"sort": sort
}
output_list.append(table)
# 排序
output_list = sorted(output_list, key=(lambda x: x["sort"]))
context["data"] = output_list
# 最后渲染
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hjl' / f"{cname}轮测试用例记录.docx"
doc.render(context)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归测试用例记录生成完毕')
from copy import deepcopy
from pathlib import Path
from ninja_extra import api_controller, ControllerBase, route
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
from django.db import transaction
from django.db.models import QuerySet
from docxtpl import DocxTemplate
from apps.dict.models import Dict
from utils.chen_response import ChenResponse
from django.shortcuts import get_object_or_404
from typing import Union
from docxtpl import InlineImage
from apps.project.models import Dut, Project, Round
from utils.util import get_list_dict, get_str_dict, get_ident, get_case_ident
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from utils.path_utils import project_path
from apps.createDocument.extensions.util import delete_dir_files
from apps.createDocument.extensions.parse_rich_text import RichParser
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
chinese_round_name: list = ['', '', '', '', '', '', '', '', '', '']
# @api_controller("/generateHSM", tags=['生成回归记录系列文档'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generateHJL", tags=['生成回归记录系列文档'])
class GenerateControllerHJL(ControllerBase):
logger = GenerateLogger('回归测试记录')
# important删除之前的文件
@route.get('/create/deleteHJLDocument', url_name='delete-hjl-document')
def delete_hjl_document(self, id: int):
project_path_str = project_path(id)
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hjl'
delete_dir_files(save_path)
@route.get("/create/basicInformation", url_name="create-basicInformation")
@transaction.atomic
def create_basicInformation(self, id: int):
"""生成回归测试记录的被测软件基本信息"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hjl' / '被测软件基本信息.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
# 第一轮次对象
round1_obj: Union[Round, None] = project_obj.pField.filter(key='0').first()
# 第一轮源代码被测件对象
round1_so_dut: Union[Dut, None] = round1_obj.rdField.filter(type='SO').first()
languages = get_list_dict('language', project_obj.language)
language_list = [item['ident_version'] for item in languages]
runtimes = get_list_dict('runtime', project_obj.runtime)
runtime_list = [item['ident_version'] for item in runtimes]
devplants = get_list_dict('devplant', project_obj.devplant)
devplant_list = [item['ident_version'] for item in devplants]
# 取非第一轮次
hround_list: QuerySet = project_obj.pField.exclude(key='0')
if len(hround_list) < 1:
# ***Inspect-start***
self.logger.model = '回归测试记录'
self.logger.write_warning_log('当前文档全部片段', f'该项目没有创建轮次')
# ***Inspect-end***
return ChenResponse(code=400, status=400, message='您未创建轮次,请创建完毕后再试')
context = {
'project_name': project_obj.name,
'language': "".join(language_list),
'soft_type': project_obj.get_soft_type_display(),
'security_level': get_str_dict(project_obj.security_level, 'security_level'),
'runtime': "".join(runtime_list),
'devplant': "".join(devplant_list),
'recv_date': project_obj.beginTime.strftime("%Y-%m-%d"),
'dev_unit': project_obj.dev_unit,
}
version_info = [{'version': round1_so_dut.version,
'line_count': int(round1_so_dut.total_lines),
'effective_line': int(round1_so_dut.effective_lines)}]
# 循环回归的轮次
for hround in hround_list:
# 每个轮次独立渲染context
context_round = deepcopy(context)
# 取中文名称
cname = chinese_round_name[int(hround.key)] # 输出二、三...
# 取该轮次源代码版本放入版本列表
so_dut: Dut = hround.rdField.filter(type='SO').first()
if not so_dut:
return ChenResponse(code=400, status=400, message=f'您第{cname}轮次中缺少源代码被测件,请添加')
version_info.append(
{'version': so_dut.version, 'line_count': int(so_dut.total_lines),
'effective_line': int(so_dut.effective_lines)})
context_round['version_info'] = version_info
# 开始渲染每个轮次的二级文档
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hjl' / f"{cname}轮被测软件基本信息.docx"
doc.render(context=context_round, autoescape=True)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归说明文档基本信息生成完毕')
@route.get("/create/caseinfo", url_name="create-caseinfo")
@transaction.atomic
def create_caseinfo(self, id: int):
"""生成回归测试记录的-{测试用例记录}"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hjl' / '测试用例记录.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
hround_list: QuerySet = project_obj.pField.exclude(key='0')
if len(hround_list) < 1:
return None
demand_prefix = '3.1'
# 循环每轮轮次对象
for hround in hround_list:
cname = chinese_round_name[int(hround.key)] # var输出二、三字样
test_type_len = Dict.objects.get(code='testType').dictItem.count() # 测试类型的个数
type_number_list = [i for i in range(1, test_type_len + 1)] # 测试类型编号对应的列表
list_list = [[] for j in range(1, test_type_len + 1)] # 每个测试类型组合为一个列表[[],[],[],[]]
testType_list, last_chapter_items = create_csx_chapter_dict(hround)
testDemands = hround.rtField.all() # 本轮所有测试项
for demand in testDemands:
type_index = type_number_list.index(int(demand.testType))
demand_ident = get_ident(demand)
# ~~~组装测试项~~~
demand_last_chapter = last_chapter_items[demand.testType].index(demand.key) + 1
demand_chapter = ".".join([demand_prefix, str(testType_list.index(demand.testType) + 1),
str(demand_last_chapter)])
demand_dict = {
'name': demand.name,
'ident': demand_ident,
'chapter': demand_chapter,
'item': []
}
# ~~~这里组装测试项里面的测试用例~~~
for case in demand.tcField.all():
step_list = []
index = 1
for one in case.step.all():
# 这里需要对operation富文本处理
rich_parser = RichParser(one.operation)
desc_list = rich_parser.get_final_list(doc, img_size=68)
rich_parser2 = RichParser(one.result)
res_list = rich_parser2.get_final_list(doc, img_size=75)
# 组装用例里面的步骤dict
passed = '通过'
if one.passed == '2':
passed = '未通过'
if one.passed == '3':
passed = '未执行'
step_dict = {
'index': index,
'operation': desc_list,
'expect': one.expect,
'result': res_list,
'passed': passed,
}
step_list.append(step_dict)
index += 1
# 查询所有的problem
problem_list = []
problem_prefix = "PT"
proj_ident = project_obj.ident
for problem in case.caseField.all():
problem_list.append("_".join([problem_prefix, proj_ident, problem.ident]))
# fpga的时序图
rich_parser3 = RichParser(case.timing_diagram)
timing_diagram = rich_parser3.get_final_list(doc, img_size=115, height=50)
has_timing_diagram = False
if len(timing_diagram) > 0:
if isinstance(timing_diagram[0], InlineImage):
has_timing_diagram = True
# 组装用例的dict
case_dict = {
'name': case.name,
'ident': get_case_ident(demand_ident, case),
'summary': case.summarize,
'initialization': case.initialization,
'premise': case.premise,
'design_person': case.designPerson,
'test_person': case.testPerson,
'monitor_person': case.monitorPerson,
'step': step_list,
'time': str(case.exe_time) if case.exe_time is not None else str(case.update_datetime),
'problems': "".join(problem_list),
'round_num_chn': cname,
# 2025年4月24日新增
'has_timing_diagram': has_timing_diagram,
'timing_diagram': timing_diagram,
}
demand_dict['item'].append(case_dict)
list_list[type_index].append(demand_dict)
# 定义渲染上下文
context = {}
output_list = []
for (index, li) in enumerate(list_list):
qs = Dict.objects.get(code="testType").dictItem.get(key=str(index + 1))
context_str = qs.title
sort = qs.sort
table = {
"type": context_str,
"item": li,
"sort": sort
}
output_list.append(table)
# 排序
output_list = sorted(output_list, key=(lambda x: x["sort"]))
context["data"] = output_list
# 最后渲染
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hjl' / f"{cname}轮测试用例记录.docx"
doc.render(context, autoescape=True)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归测试用例记录生成完毕')

File diff suppressed because it is too large Load Diff

View File

@@ -146,7 +146,7 @@ class GenerateControllerJL(ControllerBase):
output_list = sorted(output_list, key=(lambda x: x["sort"]))
context["data"] = output_list
doc.render(context)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path_str / "output_dir/jl" / "测试用例记录.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")

View File

@@ -135,7 +135,7 @@ class GenerateControllerSM(ControllerBase):
# 排序
output_list = sorted(output_list, key=(lambda x: x["sort"]))
context["data"] = output_list
doc.render(context)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path_str / "output_dir/sm" / "测试用例.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")
@@ -265,7 +265,7 @@ class GenerateControllerSM(ControllerBase):
temporary_file = Path.cwd() / 'media' / project_path_str / 'form_template' / 'sm' / 'temporary' / '说明追踪_temp.docx'
out_put_file = Path.cwd() / 'media' / project_path_str / 'output_dir' / 'sm' / '说明追踪.docx'
doc = DocxTemplate(input_file)
doc.render(context)
doc.render(context, autoescape=True)
doc.save(temporary_file)
# 通过docx合并单元格
if temporary_file.is_file():

View File

@@ -1,155 +1,155 @@
# 导入内置模块
from pathlib import Path
# 导入django、ninja等模块
from ninja_extra import api_controller, ControllerBase, route
from django.db import transaction
from django.shortcuts import get_object_or_404
# 导入文档处理模块
from docxtpl import DocxTemplate, InlineImage
# 导入ORM模型
from apps.project.models import Project
# 导入工具
from utils.util import get_str_abbr, get_str_dict
from utils.chen_response import ChenResponse
from utils.path_utils import project_path
from apps.createDocument.extensions.parse_rich_text import RichParser
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
gloger = GenerateLogger("问题单二段文档")
# @api_controller("/generateWtd", tags=['生成问题单文档系列'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller('/generateWtd', tags=['生成问题单文档系列'])
class GenerateControllerWtd(ControllerBase):
@route.get("/create/problem", url_name="create-problem")
@transaction.atomic
def create_problem(self, id: int):
"""生成问题单"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/wtd' / '问题详情表.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
problem_list = list(project_obj.projField.distinct()) # 去掉重复因为和case是多对多
problem_list.sort(key=lambda x: int(x.ident))
data_list = []
for problem in problem_list:
problem_dict = {'ident': problem.ident, 'name': problem.name}
# 1.生成被测对象名称、被测对象标识、被测对象版本
cases = problem.case.all()
# generate_log:无关联问题单进入生成日志
if cases.count() < 1:
gloger.write_warning_log('单个问题单表格', f'问题单{problem.ident}未关联用例,请检查')
str_dut_name_list = []
str_dut_ident_list = []
str_dut_version_list = []
# 2.所属用例标识
case_ident_list = []
# 3.获取依据要求
case_design_list = []
for case in cases:
if case.test.testType == '8':
# 1.1.如果为文档审查,提取所属文档名称、文档被测件标识、文档被测件版本
str_dut_name_list.append(case.dut.name)
str_dut_ident_list.append(case.dut.ref)
str_dut_version_list.append(case.dut.version)
# 对应dut名称design章节号design描述
case_design_list.append("".join([case.dut.name, case.design.chapter]))
else:
# 1.2.如果不为文档审查则提取该轮次源代码dut的信息
so_dut = case.round.rdField.filter(type='SO').first()
if so_dut:
str_dut_name_list.append(project_obj.name + '软件')
str_dut_ident_list.append(so_dut.ref)
str_dut_version_list.append(so_dut.version)
# TODO:如何处理设计需求的内容,暂时设置为取出图片,只保留文字
p_list = []
rich_parse_remove_img = RichParser(case.design.description)
rich_list = rich_parse_remove_img.get_final_list(doc)
for rich in rich_list:
if isinstance(rich, dict) or isinstance(rich, InlineImage):
continue
else:
p_list.append(rich)
case_design_list.append(
"-".join([case.dut.name, case.design.chapter + '章节' + ":" + ''.join(p_list)]))
# 2.用例标识修改-YL_测试项类型_测试项标识_用例key+1
demand = case.test # 中间变量
demand_testType = demand.testType # 中间变量
testType_abbr = get_str_abbr(demand_testType, 'testType') # 输出FT
case_ident_list.append("_".join(
['YL', testType_abbr, demand.ident, str(int(case.key[-1]) + 1).rjust(3, '0')]))
problem_dict['duts_name'] = "/".join(set(str_dut_name_list))
problem_dict['duts_ref'] = "/".join(set(str_dut_ident_list))
problem_dict['duts_version'] = "/".join(set(str_dut_version_list))
temp_name_version = []
for i in range(len(str_dut_name_list)):
temp_name_version.append(
"".join([str_dut_name_list[i] + str_dut_ident_list[i], '/V', str_dut_version_list[i]]))
problem_dict['dut_name_version'] = "\a".join(temp_name_version)
problem_dict['case_ident'] = "".join(set(case_ident_list))
problem_dict['type'] = get_str_dict(problem.type, 'problemType')
problem_dict['grade'] = get_str_dict(problem.grade, 'problemGrade')
# 依据要求-获取其设计需求
problem_dict['yaoqiu'] = "\a".join(case_design_list)
# 问题操作 - HTML解析
desc_list = ['【问题操作】']
rich_parser = RichParser(problem.operation)
desc_list.extend(rich_parser.get_final_list(doc))
# 问题影响
desc_list_result = [f'\a【问题影响】\a{problem.result}']
desc_list.extend(desc_list_result)
# 问题描述赋值
problem_dict['desc'] = desc_list
# 4.原因分析
desc_list_3 = [f'【原因分析】\a{problem.analysis}']
problem_dict['cause'] = desc_list_3
# 5.影响域分析~~~~
desc_list_4 = [f'【影响域分析】\a{problem.effect_scope}']
problem_dict['effect_scope'] = desc_list_4
# 6.改正措施
problem_dict['solve'] = problem.solve
# 7.回归验证结果
desc_list_5 = []
rich_parser5 = RichParser(problem.verify_result)
desc_list_5.extend(rich_parser5.get_final_list(doc))
problem_dict['verify_result'] = desc_list_5
# 8.其他日期和人员
problem_dict['postPerson'] = problem.postPerson
problem_dict['postDate'] = problem.postDate
close_str = '□修改文档 □修改程序 □不修改'
if len(problem.closeMethod) < 1:
close_str = '□修改文档 □修改程序 ■不修改'
elif len(problem.closeMethod) == 2:
close_str = '■修改文档 ■修改程序 □不修改'
else:
if problem.closeMethod[0] == '1':
close_str = '■修改文档 □修改程序 □不修改'
elif problem.closeMethod[0] == '2':
close_str = '□修改文档 ■修改程序 □不修改'
else:
close_str = '□修改文档 □修改程序 □不修改'
problem_dict['closeMethod'] = close_str
problem_dict['designer'] = problem.designerPerson
problem_dict['designDate'] = problem.designDate
problem_dict['verifyPerson'] = problem.verifyPerson
problem_dict['verifyDate'] = problem.verifyDate
data_list.append(problem_dict)
context = {
'project_name': project_obj.name,
'project_ident': project_obj.ident,
'problem_list': data_list,
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path_str / "output_dir/wtd" / '问题详情表.docx')
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 导入内置模块
from pathlib import Path
from django.db.models import QuerySet
# 导入django、ninja等模块
from ninja_extra import api_controller, ControllerBase, route
from django.db import transaction
from django.shortcuts import get_object_or_404
# 导入文档处理模块
from docxtpl import DocxTemplate, InlineImage
# 导入ORM模型
from apps.project.models import Project, Case
# 导入工具
from utils.util import get_str_abbr, get_str_dict
from utils.chen_response import ChenResponse
from utils.path_utils import project_path
from apps.createDocument.extensions.parse_rich_text import RichParser
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
gloger = GenerateLogger("问题单二段文档")
# @api_controller("/generateWtd", tags=['生成问题单文档系列'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller('/generateWtd', tags=['生成问题单文档系列'])
class GenerateControllerWtd(ControllerBase):
@route.get("/create/problem", url_name="create-problem")
@transaction.atomic
def create_problem(self, id: int):
"""生成问题单"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/wtd' / '问题详情表.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
problem_list = list(project_obj.projField.distinct()) # 去掉重复因为和case是多对多
problem_list.sort(key=lambda x: int(x.ident))
data_list = []
for problem in problem_list:
problem_dict = {'ident': problem.ident, 'name': problem.name}
# 1.生成被测对象名称、被测对象标识、被测对象版本
cases: QuerySet[Case] = problem.case.all()
# generate_log:无关联问题单进入生成日志
if cases.count() < 1:
gloger.write_warning_log('单个问题单表格', f'问题单{problem.ident}未关联用例,请检查')
str_dut_name_list = []
str_dut_ident_list = []
str_dut_version_list = []
# 2.所属用例标识
case_ident_list = []
# 3.获取依据要求
case_design_list = []
for case in cases:
if case.test.testType == '8':
# 1.1.如果为文档审查,提取所属文档名称、文档被测件标识、文档被测件版本
str_dut_name_list.append(case.dut.name)
str_dut_ident_list.append(case.dut.ref)
str_dut_version_list.append(case.dut.version)
# 对应dut名称design章节号design描述
case_design_list.append("".join([case.dut.name, case.design.chapter]))
else:
# 1.2.如果不为文档审查则提取该轮次源代码dut的信息
so_dut = case.round.rdField.filter(type='SO').first()
if so_dut:
str_dut_name_list.append(project_obj.name + '软件')
str_dut_ident_list.append(so_dut.ref)
str_dut_version_list.append(so_dut.version)
# TODO:如何处理设计需求的内容,暂时设置为取出图片,只保留文字
p_list = []
rich_parse_remove_img = RichParser(case.design.description)
rich_list = rich_parse_remove_img.get_final_list(doc)
for rich in rich_list:
if isinstance(rich, dict) or isinstance(rich, InlineImage):
continue
else:
p_list.append(rich)
case_design_list.append("-".join([case.dut.name, case.design.chapter + '章节' + ":" + ''.join(p_list)]))
# 2.用例标识修改-YL_测试项类型_测试项标识_用例key+1
demand = case.test # 中间变量
demand_testType = demand.testType # 中间变量
testType_abbr = get_str_abbr(demand_testType, 'testType') # 输出FT
case_ident_list.append("_".join(
['YL', testType_abbr, demand.ident, str(int(case.key[-1]) + 1).rjust(3, '0')]))
problem_dict['duts_name'] = "/".join(set(str_dut_name_list))
problem_dict['duts_ref'] = "/".join(set(str_dut_ident_list))
problem_dict['duts_version'] = "/".join(set(str_dut_version_list))
temp_name_version = []
for i in range(len(str_dut_name_list)):
temp_name_version.append(
"".join([str_dut_name_list[i] + str_dut_ident_list[i], '/V', str_dut_version_list[i]]))
problem_dict['dut_name_version'] = "\a".join(set(temp_name_version))
problem_dict['case_ident'] = "".join(set(case_ident_list))
problem_dict['type'] = get_str_dict(problem.type, 'problemType')
problem_dict['grade'] = get_str_dict(problem.grade, 'problemGrade')
# 依据要求-获取其设计需求
print(case_design_list)
problem_dict['yaoqiu'] = "\a".join(case_design_list)
# 问题操作 - HTML解析
desc_list = ['【问题操作】']
rich_parser = RichParser(problem.operation)
desc_list.extend(rich_parser.get_final_list(doc))
# 问题影响
desc_list_result = [f'\a【问题影响】\a{problem.result}']
desc_list.extend(desc_list_result)
# 问题描述赋值
problem_dict['desc'] = desc_list
# 4.原因分析
desc_list_3 = [f'【原因分析】\a{problem.analysis}']
problem_dict['cause'] = desc_list_3
# 5.影响域分析~~~~
desc_list_4 = [f'【影响域分析】\a{problem.effect_scope}']
problem_dict['effect_scope'] = desc_list_4
# 6.改正措施
problem_dict['solve'] = problem.solve
# 7.回归验证结果
desc_list_5 = []
rich_parser5 = RichParser(problem.verify_result)
desc_list_5.extend(rich_parser5.get_final_list(doc))
problem_dict['verify_result'] = desc_list_5
# 8.其他日期和人员
problem_dict['postPerson'] = problem.postPerson
problem_dict['postDate'] = problem.postDate
close_str = '□修改文档 □修改程序 □不修改'
if len(problem.closeMethod) < 1:
close_str = '□修改文档 □修改程序 ■不修改'
elif len(problem.closeMethod) == 2:
close_str = '■修改文档 ■修改程序 □不修改'
else:
if problem.closeMethod[0] == '1':
close_str = '■修改文档 □修改程序 □不修改'
elif problem.closeMethod[0] == '2':
close_str = '□修改文档 ■修改程序 □不修改'
else:
close_str = '□修改文档 □修改程序 □不修改'
problem_dict['closeMethod'] = close_str
problem_dict['designer'] = problem.designerPerson
problem_dict['designDate'] = problem.designDate
problem_dict['verifyPerson'] = problem.verifyPerson
problem_dict['verifyDate'] = problem.verifyDate
data_list.append(problem_dict)
context = {
'project_name': project_obj.name,
'project_ident': project_obj.ident,
'problem_list': data_list,
}
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path_str / "output_dir/wtd" / '问题详情表.docx')
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))

View File

@@ -1,7 +1,9 @@
from apps.project.models import Project
from apps.project.models import Project, Round, InfluenceArea
from docxtpl import DocxTemplate
from utils.util import *
from utils.chen_response import ChenResponse
from django.db.models import Q
from apps.createDocument.extensions.parse_rich_text import RichParser
def create_round_context(project_obj: Project, round_id: str):
"""根据轮次,生成测评报告中的测评结果"""
@@ -77,4 +79,37 @@ def create_round_context(project_obj: Project, round_id: str):
'r2_dynamic_str': r2_dynamic_str,
'round_id': round_chinese[round_id],
}
return context
return context, round_obj
# ~~~影响域分析内容返回influence的render_list~~~
def create_influence_context(doc: DocxTemplate, round_obj: Round, project_obj: Project) -> None | list:
area_qs = InfluenceArea.objects.filter(round=round_obj)
item_render_list = []
## 如果存在则查询items
if area_qs.exists():
area_obj = area_qs.first()
items_qs = area_obj.influence_items.all()
if items_qs.exists():
index = 1
for item in items_qs:
# 1.处理关联case - 找第一轮cases
case_str_list = []
for case in project_obj.pcField.filter(key__in=item.effect_cases):
case_ident_index = str(int(case.key.split("-")[-1]) + 1).zfill(3)
case_str_list.append("_".join(["YL", get_str_abbr(case.test.testType, "testType"), case.ident, case_ident_index]))
# 2.处理富文本框
parser = RichParser(item.change_des)
item_dict = {
"change_type": item.change_type,
"change_influ": item.change_influ,
"case_str_list": case_str_list,
"change_des": parser.get_final_list(doc, img_size=40, height=30), # 富文本未处理
"index": str(index),
}
index = index + 1
item_render_list.append(item_dict)
if len(item_render_list) > 0:
return item_render_list
else:
return None

View File

@@ -1,127 +1,143 @@
"""
专门解析富文本插件tinymce的html内容
"""
import pandas as pd
from bs4 import BeautifulSoup
from bs4.element import Tag, NavigableString
import base64
import io
from docxtpl import InlineImage
from docx.shared import Mm, Cm
import re
# text.replace('\xa0', ' '))
class RichParser:
def __init__(self, rich_text):
# 将rich_text的None变为空字符串鲁棒
if rich_text is None:
rich_text = ""
# 对原始html解析后的bs对象
self.bs = BeautifulSoup(rich_text, 'html.parser')
self.content = self.remove_n_in_contents()
# 最终的解析后的列表
self.data_list = []
self.line_parse()
# 1.函数将self.bs.contents去掉\n获取每行数据
def remove_n_in_contents(self):
content_list = []
for line in self.bs.contents:
if line != '\n':
content_list.append(line)
return content_list
# 2.逐个遍历self.content去掉table元素Tag对象单独解析
def line_parse(self):
for tag in self.content:
if isinstance(tag, NavigableString):
self.data_list.append(tag.text)
elif isinstance(tag, Tag):
if tag.name == 'p':
img_list = tag.find_all('img')
if len(img_list) > 0:
for img_item in img_list:
self.data_list.append(img_item.get('src'))
else:
self.data_list.append(tag.text)
elif tag.name == 'table':
df_dict_list = self.parse_tag2list(tag)
self.data_list.append(df_dict_list)
elif tag.name == 'div':
table_list = tag.find_all('table')
if len(table_list) > 0:
for table in table_list:
df_dict_list = self.parse_tag2list(table)
self.data_list.append(df_dict_list)
# 3.1.辅助方法,将<table>的Tag对象转为[[]]二维列表格式
def parse_tag2list(self, table_tag):
# str(tag)可直接变成<table>xxx</table>
pd_list = pd.read_html(io.StringIO(str(table_tag)))
# 将dataframe变为数组
df = pd_list[0]
# 处理第一行为数字的情况,如果为数字则删除第一行,让第二行为列名
if all(isinstance(col, int) for col in df.columns):
df.columns = df.iloc[0]
df = df.drop(0) # 删除原来的第一行
# 转为列表的列表(二维列表)
# return df.values.tolist()
return df.fillna('').T.reset_index().T.values.tolist()
# 3.2.辅助方法,打印解析后列表
def print_content(self):
for line in self.data_list:
print(line)
# 4.1.最终方法生成给docxtpl可用的列表 -> 注意需要传递DocxTemplate对象在接口函数里面初始化的
def get_final_list(self, doc, /, *, img_size=100, height=80):
"""注意关键字传参可修改图片大小img_size:int=100"""
final_list = []
for oneline in self.data_list:
# 这里要单独处理下二维列表
if isinstance(oneline, list):
final_list.append({'isTable': True, 'data': oneline})
continue
if oneline.startswith("data:image/png;base64"):
base64_bytes = base64.b64decode(oneline.replace("data:image/png;base64,", ""))
# ~~~设置了固定宽度、高度~~~
final_list.append(InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(img_size), height=Mm(height)))
else:
final_list.append(oneline)
if len(final_list) <= 0:
final_list.append("")
return final_list
# 4.2.最终方法,在上面方法基础上,增加格式,例如<p>增加缩进,图片居中,<p>包含“图x”则居中
def get_final_format_list(self, doc, /, *, img_size=115, height=80):
final_list = []
for oneline in self.data_list:
# 这里要单独处理下二维列表
if isinstance(oneline, list):
final_list.append({'isTable': True, 'data': oneline})
continue
if oneline.startswith("data:image/png;base64"):
base64_bytes = base64.b64decode(oneline.replace("data:image/png;base64,", ""))
# 1.和上面函数变化图片更改为dict然后isCenter属性居中
final_list.append(
{'isCenter': True,
'data': InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(img_size), height=height)})
else:
# 2.和上面区别:如果<p>带有“图”则居中
if re.match(r"[表图]\d.*", oneline):
final_list.append({"isCenter": True, "data": oneline})
else:
final_list.append({"isCenter": False, "data": oneline})
if len(final_list) <= 0:
final_list.append("")
return final_list
# 5.最终方法去掉图片和table元素 -> 纯文本列表
def get_final_p_list(self):
final_list = []
for oneline in self.data_list:
if isinstance(oneline, list) or oneline.startswith("data:image/png;base64"):
continue
else:
final_list.append(oneline)
return final_list
"""
专门解析富文本插件tinymce的html内容
"""
import pandas as pd
from bs4 import BeautifulSoup
from bs4.element import Tag, NavigableString
import base64
import io
from docxtpl import InlineImage
from docx.shared import Mm
import re
# text.replace('\xa0', ' '))
class RichParser:
def __init__(self, rich_text):
# 将rich_text的None变为空字符串鲁棒
if rich_text is None:
rich_text = ""
# 对原始html解析后的bs对象
self.bs = BeautifulSoup(rich_text, 'html.parser')
self.content = self.remove_n_in_contents()
# 最终的解析后的列表
self.data_list = []
self.line_parse()
# 匹配“表1-3”或“表1”等字符的正则
self.biao_pattern = re.compile(r"\d+(?:-\d+)?")
# 1.函数将self.bs.contents去掉\n获取每行数据
def remove_n_in_contents(self):
content_list = []
for line in self.bs.contents:
if line != '\n':
content_list.append(line)
return content_list
# 2.逐个遍历self.content去掉table元素Tag对象单独解析
def line_parse(self):
for tag in self.content:
if isinstance(tag, NavigableString):
self.data_list.append(tag.text)
elif isinstance(tag, Tag):
if tag.name == 'p':
img_list = tag.find_all('img')
if len(img_list) > 0:
for img_item in img_list:
self.data_list.append(img_item.get('src'))
else:
self.data_list.append(tag.text)
elif tag.name == 'table':
df_dict_list = self.parse_tag2list(tag)
self.data_list.append(df_dict_list)
elif tag.name == 'div':
table_list = tag.find_all('table')
if len(table_list) > 0:
for table in table_list:
df_dict_list = self.parse_tag2list(table)
self.data_list.append(df_dict_list)
# 3.1.辅助方法,将<table>的Tag对象转为[[]]二维列表格式
def parse_tag2list(self, table_tag):
# str(tag)可直接变成<table>xxx</table>
pd_list = pd.read_html(io.StringIO(str(table_tag)))
# 将dataframe变为数组
df = pd_list[0]
# 处理第一行为数字的情况,如果为数字则删除第一行,让第二行为列名
if all(isinstance(col, int) for col in df.columns):
df.columns = df.iloc[0]
df = df.drop(0) # 删除原来的第一行
# 转为列表的列表(二维列表)
# return df.values.tolist()
return df.fillna('').T.reset_index().T.values.tolist()
# 3.2.辅助方法,打印解析后列表
def print_content(self):
for line in self.data_list:
print(line)
# 4.1.最终方法生成给docxtpl可用的列表 -> 注意需要传递DocxTemplate对象在接口函数里面初始化的
def get_final_list(self, doc, /, *, img_size=100, height=80):
"""注意关键字传参可修改图片大小img_size:int=100"""
final_list = []
for oneline in self.data_list:
# 这里要单独处理下二维列表
if isinstance(oneline, list):
final_list.append({'isTable': True, 'data': oneline})
continue
if oneline.startswith("data:image/png;base64") or oneline.startswith("data:image/jpeg;base64,") or oneline.startswith(
"data:image/jpg;base64,"):
base64_bytes = base64.b64decode(oneline.replace("data:image/png;base64,", ""))
# ~~~设置了固定宽度、高度~~~
inline_image = InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(img_size), height=Mm(height))
final_list.append(inline_image)
else:
# ~~~新增:将\xa0修改为普通空格~~~
oneline = oneline.replace('\xa0', ' ')
final_list.append(oneline)
if len(final_list) <= 0:
final_list.append("")
# 针对tinymce中粘贴表格最后一行显示句号问题这里统一删除
if final_list[-1] == '\xa0':
final_list.pop()
return final_list
# 4.2.最终方法,在上面方法基础上,增加格式,例如<p>增加缩进,图片居中,<p>包含“图x”则居中
def get_final_format_list(self, doc, /, *, img_size=115, height=80):
final_list = []
for oneline in self.data_list:
# 这里要单独处理下二维列表
if isinstance(oneline, list):
final_list.append({'isTable': True, 'data': oneline})
continue
if oneline.startswith("data:image/png;base64"):
base64_bytes = base64.b64decode(oneline.replace("data:image/png;base64,", ""))
# 1.和上面函数变化图片更改为dict然后isCenter属性居中
final_list.append(
{'isCenter': True,
'data': InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(img_size), height=height)})
else:
# 2.和上面区别:如果<p>带有“图”则居中
if re.match(r"[表图]\d.*", oneline):
final_list.append({"isCenter": True, "data": oneline.replace('\xa0', ' ')})
else:
final_list.append({"isCenter": False, "data": oneline.replace('\xa0', ' ')})
if len(final_list) <= 0:
final_list.append("")
return final_list
# 5.最终方法去掉图片和table元素 -> 纯文本列表
def get_final_p_list(self):
final_list = []
for oneline in self.data_list:
if isinstance(oneline, list) or oneline.startswith("data:image/png;base64"):
continue
cleaned_line = oneline
cleaned_line = re.sub(r'\s+', '', cleaned_line)
cleaned_line = cleaned_line.replace(')', '')
cleaned_line = cleaned_line.strip()
# 去掉以“表3”的行
if self.biao_pattern.search(cleaned_line):
continue
if cleaned_line:
final_list.append(cleaned_line)
return final_list

View File

@@ -0,0 +1,142 @@
from apps.project.models import TestDemand
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.table import _Cell, Table
def demand_sort_by_designKey(demand_obj: TestDemand) -> tuple[int, ...]:
"""仅限于测试项排序函数传入sorted函数的key里面"""
parts = demand_obj.key.split('-')
sort_tuple = tuple(int(part) for part in parts)
return sort_tuple
# 传入cell设置边框
def set_cell_border(cell: _Cell, **kwargs):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
# 检查标签是否存在,如果没有找到,则创建一个
tcBorders = tcPr.first_child_found_in("w:tcBorders")
if tcBorders is None:
tcBorders = OxmlElement('w:tcBorders')
tcPr.append(tcBorders)
for border_type in ['left', 'top', 'right', 'bottom']:
# 设置为固定的“黑色加粗”
border_data = kwargs.get(border_type, {"sz": "6", "val": "single", "color": "#000000", "space": "0"})
tag = 'w:{}'.format(border_type)
element = tcBorders.find(qn(tag))
if element is None:
element = OxmlElement(tag)
tcBorders.append(element)
for key in ["sz", "val", "color", "space", "shadow"]:
if key in border_data:
element.set(qn('w:{}'.format(key)), str(border_data[key]))
# 弃用,请使用下面函数
def set_table_border(table, **kwargs):
"""docx-设置表格上下左右边框"""
# 获取或创建表格属性
tbl_pr = table._tbl.tblPr
# 查找并移除现有的边框设置
existing_borders = tbl_pr.find(qn('w:tblBorders'))
if existing_borders is not None:
tbl_pr.remove(existing_borders)
# 创建新的边框元素
borders = OxmlElement('w:tblBorders')
# 只设置外边框top, left, bottom, right - 设置为固定“黑色加粗”
# 不设置 insideV 和 insideH内部边框
for border_type in ['top', 'left', 'bottom', 'right']:
border_data = kwargs.get(border_type, {"sz": "12", "val": "single", "color": "#000000"})
border_elem = OxmlElement(f'w:{border_type}')
# 设置边框属性
border_elem.set(qn('w:val'), border_data.get('val', 'single')) # 线条类型
border_elem.set(qn('w:sz'), border_data.get('sz', '12')) # 线条粗细8代表1磅
border_elem.set(qn('w:color'), border_data.get('color', '#000000')) # 颜色
borders.append(border_elem) # type:ignore
# 将边框设置添加到表格属性中
tbl_pr.append(borders)
# ~~~新解决方案传入table对象遍历cell判断cell是否在外层~~~
def set_table_border_by_cell_position(table: Table):
"""
智能设置表格边框:外边框粗,内边框细。
"""
# 获取表格的总行数和总列数
total_rows = len(table.rows)
total_cols = len(table.columns)
for row_idx, row in enumerate(table.rows):
for col_idx, cell in enumerate(row.cells):
# 初始化边框参数字典
border_kwargs = {}
# 1. 判断上边框:如果是第一行,则设置粗上边框,否则不设置(由上一行的下边框决定,或单独设置细线)
if row_idx == 0:
border_kwargs['top'] = {"sz": "12", "val": "single", "color": "#000000"}
# 2. 判断下边框:如果是最后一行,则设置粗下边框
if row_idx == total_rows - 1:
border_kwargs['bottom'] = {"sz": "12", "val": "single", "color": "#000000"}
# 3. 判断左边框:如果是第一列,则设置粗左边框
if col_idx == 0:
border_kwargs['left'] = {"sz": "12", "val": "single", "color": "#000000"}
# 4. 判断右边框:如果是最后一列,则设置粗右边框
if col_idx == total_cols - 1:
border_kwargs['right'] = {"sz": "12", "val": "single", "color": "#000000"}
# 5. 设置内部网格线(细线)
# 内部横线 (insideH): 所有单元格都需要,但最后一行不需要(已经是外边框)
if row_idx < total_rows - 1:
border_kwargs['insideH'] = {"sz": "6", "val": "single", "color": "#000000"}
# 内部竖线 (insideV): 所有单元格都需要,但最后一列不需要(已经是外边框)
if col_idx < total_cols - 1:
border_kwargs['insideV'] = {"sz": "6", "val": "single", "color": "#000000"}
# 调用您已有的 set_cell_border 函数
set_cell_border(cell, **border_kwargs)
# 设置cell的左右边距
def set_cell_margins(cell: _Cell, **kwargs):
"""
设置单元格边距确保在Office和WPS中均能生效。
参数示例: set_cell_margins(cell, left=50, right=50, top=100, bottom=100)
参数单位: 为二十分之一磅 (dxa, 1/1440英寸)。
"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
# 关键步骤1检查或创建 w:tcMar 元素
tcMar = tcPr.find(qn('w:tcMar'))
if tcMar is None:
tcMar = OxmlElement('w:tcMar')
tcPr.append(tcMar)
# 关键步骤2为每个指定的边距方向创建元素并同时设置新旧两套属性以保证兼容性[2](@ref)
# 定义映射:我们的参数名 -> (XML元素名, 备用的XML元素名)
margin_map = {
'left': ('left', 'start'),
'right': ('right', 'end'),
'top': ('top', None),
'bottom': ('bottom', None)
}
for margin_key, value in kwargs.items():
if margin_key in margin_map:
primary_tag, alternate_tag = margin_map[margin_key]
tags_to_set = [primary_tag]
if alternate_tag: # 如果存在备选标签如left/start则同时设置
tags_to_set.append(alternate_tag)
for tag in tags_to_set:
# 检查该边距元素是否已存在
margin_element = tcMar.find(qn(f'w:{tag}'))
if margin_element is None:
margin_element = OxmlElement(f'w:{tag}')
tcMar.append(margin_element) # type:ignore
# 设置边距值和单位类型
margin_element.set(qn('w:w'), str(value))
margin_element.set(qn('w:type'), 'dxa')

View File

@@ -1,97 +1,109 @@
from pathlib import Path
from docxtpl import DocxTemplate
from docx.table import Table
from utils.chen_response import ChenResponse
from typing import Any
from apps.project.models import Project
from utils.path_utils import project_path
def merge_all_cell(table: Table) -> None:
"""生成需求研总对照表工具逐个找第二列和第三列单元格的text如果一致则合并"""
col_list = [table.columns[1], table.columns[2]]
# 合并第二列相同的单元格
for col_right in col_list:
index = 0
temp_text = ""
for cell in col_right.cells:
if index == 0:
temp_text = cell.text
else:
if cell.text == temp_text:
if cell.text == '': # 不知道什么原因必须这样判断下
cell.text = '/'
text_temp = cell.text
ce = cell.merge(col_right.cells[index - 1])
ce.text = text_temp
else:
temp_text = cell.text
index += 1
def create_sm_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终说明文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'sm' / template_name
doc = DocxTemplate(input_path)
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/sm" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_dg_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终大纲文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / template_name
doc = DocxTemplate(input_path)
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_bg_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终报告文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'bg' / template_name
doc = DocxTemplate(input_path)
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/bg" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_wtd_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终问题单文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'wtd' / template_name
doc = DocxTemplate(input_path)
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/wtd" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def get_round1_problem(project: Project) -> Any:
"""
从项目返回第一轮问题单
:param project: Project项目Model对象
:return: 问题单的列表
"""
all_problem_qs = project.projField.all()
# 遍历每个问题,找出第一轮的问题
problem_set = set()
for problem in all_problem_qs:
flag = False
for case in problem.case.all():
if case.round.key == '0':
flag = True
if flag:
problem_set.add(problem)
return list(problem_set)
def delete_dir_files(path: Path) -> Any:
"""传入一个Path对象如果是文件夹则删除里面所有的文件不删除文件夹"""
if path.is_dir():
for file in path.iterdir():
if file.is_file():
file.unlink()
from pathlib import Path
from docxtpl import DocxTemplate
from docx.table import Table
from utils.chen_response import ChenResponse
from typing import Any
from apps.project.models import Project
from utils.path_utils import project_path
def merge_all_cell(table: Table) -> None:
"""生成需求研总对照表工具逐个找第二列和第三列单元格的text如果一致则合并"""
col_list = [table.columns[1], table.columns[2]]
# 合并第二列相同的单元格
for col_right in col_list:
index = 0
temp_text = ""
for cell in col_right.cells:
if index == 0:
temp_text = cell.text
else:
if cell.text == temp_text:
if cell.text == '': # 不知道什么原因必须这样判断下
cell.text = '/'
text_temp = cell.text
ce = cell.merge(col_right.cells[index - 1])
ce.text = text_temp
else:
temp_text = cell.text
index += 1
def create_sm_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终说明文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'sm' / template_name
doc = DocxTemplate(input_path)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/sm" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_hsm_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终回归测试说明文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'hsm' / template_name
doc = DocxTemplate(input_path)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/hsm" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_dg_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终大纲文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / template_name
doc = DocxTemplate(input_path)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_bg_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终报告文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'bg' / template_name
doc = DocxTemplate(input_path)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/bg" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def create_wtd_docx(template_name: str, context: dict, id: int) -> ChenResponse:
"""生成最终问题单文档工具函数"""
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'wtd' / template_name
doc = DocxTemplate(input_path)
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/wtd" / template_name)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
def get_round1_problem(project: Project) -> Any:
"""
从项目返回第一轮问题单
:param project: Project项目Model对象
:return: 问题单的列表
"""
all_problem_qs = project.projField.all()
# 遍历每个问题,找出第一轮的问题
problem_set = set()
for problem in all_problem_qs:
flag = False
for case in problem.case.all():
if case.round.key == '0':
flag = True
if flag:
problem_set.add(problem)
return list(problem_set)
def delete_dir_files(path: Path) -> Any:
"""传入一个Path对象如果是文件夹则删除里面所有的文件不删除文件夹"""
if path.is_dir():
for file in path.iterdir():
if file.is_file():
file.unlink()

File diff suppressed because it is too large Load Diff

View File

@@ -1,348 +1,352 @@
"""该文件是:替换文档片段然后生成辅助生成最终文档"""
from io import BytesIO
from typing import List, Dict
from pathlib import Path
from docx import Document
from docx.text.paragraph import Paragraph
from docx.table import Table
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.oxml.text.run import CT_R
from docx.oxml.shape import CT_Picture
from docx.parts.image import ImagePart
from docx.text.run import Run
from docx.shared import Mm
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from lxml.etree import _Element
# 路径工具
from utils.path_utils import project_path
### 模块变量:定义常用图片所在区域的宽高
Demand_table_xqms = Mm(134) # 1.测评大纲-测试项里面-需求描述单元格
Timing_diagram_width = Mm(242) # 2.测试记录-时序图
Test_result_width = Mm(78) # 3.测试记录-测试结果
Horizatal_width = Mm(130) # 4.所有文档-页面图片的横向距离(图片宽度预设置)
def getParentRunNode(node):
"""传入oxml节点对象获取其祖先节点的CT_R"""
if isinstance(node, CT_R):
return node
return getParentRunNode(node.getparent())
def generate_temp_doc(doc_type: str, project_id: int, round_num=None, frag_list=None):
""" 该函数参数:
:param frag_list: 储存用户不覆盖的片段列表
:param round_num: 只有回归说明和回归记录有
:param project_id: 项目id
:param doc_type:大纲 sm:说明 jl:记录 bg:报告 hsm:回归测试说明 hjl:回归测试记录,默认路径为dg -> 所以如果传错就生成生成大纲了
:return (to_tpl_file路径, seitai_final_file路径)
"""
if frag_list is None:
frag_list = []
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
project_path_str = project_path(project_id)
# 根据传入需要处理的文档类型,自动获路径
prefix = Path.cwd() / 'media' / project_path_str
template_file: Path = prefix / 'form_template' / 'products' / '测评大纲.docx'
to_tpl_file: Path = prefix / 'temp' / '测评大纲.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测评大纲.docx'
if doc_type == 'sm':
template_file = prefix / 'form_template' / 'products' / '测试说明.docx'
to_tpl_file = prefix / 'temp' / '测试说明.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测试说明.docx'
elif doc_type == 'jl':
template_file = prefix / 'form_template' / 'products' / '测试记录.docx'
to_tpl_file = prefix / 'temp' / '测试记录.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测试记录.docx'
elif doc_type == 'bg':
template_file = prefix / 'form_template' / 'products' / '测评报告.docx'
to_tpl_file = prefix / 'temp' / '测评报告.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测评报告.docx'
elif doc_type == 'hsm':
# 如果products里面存在“用户上传的第n轮回归测试说明.docx则使用它作为模版”
template_file = prefix / 'form_template' / 'products' / f'{round_num}轮回归测试说明.docx'
if not template_file.exists():
template_file = prefix / 'form_template' / 'products' / '回归测试说明.docx'
to_tpl_file = prefix / 'temp' / f'{round_num}轮回归测试说明.docx'
seitai_final_file: Path = prefix / 'final_seitai' / f'{round_num}轮回归测试说明.docx'
elif doc_type == 'hjl':
# 如果products里面存在“用户上传的第n轮回归测试记录.docx则使用它作为模版”
template_file = prefix / 'form_template' / 'products' / f'{round_num}轮回归测试记录.docx'
if not template_file.exists():
template_file = prefix / 'form_template' / 'products' / '回归测试记录.docx'
to_tpl_file = prefix / 'temp' / f'{round_num}轮回归测试记录.docx'
seitai_final_file: Path = prefix / 'final_seitai' / f'{round_num}轮回归测试记录.docx'
elif doc_type == 'wtd':
template_file = prefix / 'form_template' / 'products' / '问题单.docx'
to_tpl_file = prefix / 'temp' / '问题单.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '问题单.docx'
# 定义找寻被复制文件根路径 - 后续会根据type找子路径
output_files_path = prefix / 'output_dir'
# 这里可能修改,储存大纲里面的文档片段
dg_copied_files = []
# 储存sm/jl/hsm/hjl/bg/wtd的文档片段
exclusive_copied_files = []
# 新储存reuse的文档片段
reuse_files = []
# 将被拷贝文件分别放入不同两个数组
for file in output_files_path.iterdir():
if file.is_file():
if file.suffix == '.docx':
dg_copied_files.append(file)
elif file.is_dir():
# 如果文件夹名称为sm/jl/hsm/hjl/bg/wtd则进入该判断
# 所以要求文件系统文件夹名称必须是sm/jl/hsm/hjl/bg/wtd不然无法生成
if file.stem == doc_type:
for f in file.iterdir():
if f.suffix == '.docx':
exclusive_copied_files.append(f)
for file in (prefix / 'reuse').iterdir():
if file.is_file():
if file.suffix == '.docx':
reuse_files.append(file)
# 找到基础模版的所有std域
doc = Document(template_file.as_posix())
body = doc.element.body
sdt_element_list = body.xpath('./w:sdt')
# 找到sdt域的名称 -> 为了对应output_dir文件 / 储存所有output_dir图片
area_name_list = []
image_part_list = [] # 修改为字典两个字段{ 'name':'测评对象', 'img':ImagePart }
# 筛选片段【二】:用户前端要求不要覆盖的文档片段
frag_is_cover_dict = {item.name: item.isCover for item in frag_list}
# 遍历所有控件 -> 放入area_name_list【这里准备提取公共代码】
for sdt_ele in sdt_element_list:
isLock = False
for elem in sdt_ele.iterchildren():
# 【一】用户设置lock - 下面2个if将需要被替换的(控件名称)存入area_name_list
if elem.tag.endswith('sdtPr'):
for el in elem.getchildren():
if el.tag.endswith('lock'):
isLock = True
if elem.tag.endswith('sdtPr'):
for el in elem.getchildren():
if el.tag.endswith('alias'):
# 筛序【一】取出用户设置lock的文档片段
if len(el.attrib.values()) > 0 and (isLock == False):
area_name = el.attrib.values()[0]
# 筛选【二】:前端用户选择要覆盖的片段
if frag_is_cover_dict.get(area_name):
area_name_list.append(area_name)
# 下面开始替换area_name_list的“域”这时已经被筛选-因为sdtPr和sdtContent是成对出现
if elem.tag.endswith('sdtContent'):
if len(area_name_list) > 0:
# 从第一个片段名称开始取,取到模版的“域”名称
area_pop_name = area_name_list.pop(0)
# 这里先去找media/output_dir/xx下文件然后找media/output下文件
copied_file_path = ""
# 下面if...else是找output_dir下面文件与“域”名称匹配匹配到存入copied_file_path
if doc_type == 'dg':
for file in dg_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
else:
# 如果不是大纲
if round_num is None:
# 如果非回归说明、记录
for file in exclusive_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
# 这里判断是否copied_file_path没取到文件然后遍历reuse下文件
if not copied_file_path:
for file in reuse_files:
if file.stem == area_pop_name:
copied_file_path = file
# 如果上面被复制文件还没找到然后遍历output_dir下文件
if not copied_file_path:
for file in dg_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
else:
# 因为回归的轮次,前面会加 -> 第{round_num}轮
for file in exclusive_copied_files: # 这里多了第{round_num}轮
if file.stem == f"{round_num}{area_pop_name}":
copied_file_path = file
if not copied_file_path:
for file in reuse_files:
if file.stem == area_pop_name:
copied_file_path = file
if not copied_file_path:
for file in dg_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
# 找到文档片段.docx将其数据复制到对应area_name的“域”
if copied_file_path:
doc_copied = Document(copied_file_path)
copied_element_list = []
element_list = doc_copied.element.body.inner_content_elements
for elet in element_list:
if isinstance(elet, CT_P):
copied_element_list.append(Paragraph(elet, doc_copied))
if isinstance(elet, CT_Tbl):
copied_element_list.append(Table(elet, doc_copied))
elem.clear()
for para_copied in copied_element_list:
elem.append(para_copied._element)
# 下面代码就是将图片全部提取到image_part_list以便后续插入注意这时候已经是筛选后的
doc_copied = Document(copied_file_path) # 需要重新获取否则namespace错误
copied_body = doc_copied.element.body
img_node_list = copied_body.xpath('.//pic:pic')
if not img_node_list:
pass
else:
for img_node in img_node_list:
img: CT_Picture = img_node
# 根据节点找到图片的关联id
embed = img.xpath('.//a:blip/@r:embed')[0]
# 这里得到ImagePart -> 马上要给新文档添加
related_part: ImagePart = doc_copied.part.related_parts[embed]
# doc_copied.part.related_parts是一个字典
image_part_list.append({'name': area_pop_name, 'img': related_part})
# 现在是替换后找到替换后文档所有pic:pic并对“域”名称进行识别
graph_node_list = body.xpath('.//pic:pic')
graph_node_list_transform = []
for picNode in graph_node_list:
# 遍历替换后模版的所有pic去找祖先
sdt_node = picNode.xpath('ancestor::w:sdt[1]')[0]
for sdt_node_child in sdt_node.iterchildren():
# 找到sdt下一级的stdPr
if sdt_node_child.tag.endswith('sdtPr'):
for sdtPr_node_child in sdt_node_child.getchildren():
if sdtPr_node_child.tag.endswith('alias'):
yu_name = sdtPr_node_child.attrib.values()[0]
graph_node_list_transform.append({'yu_name': yu_name, 'yu_node': picNode})
for graph_node in graph_node_list_transform:
image_run_node = getParentRunNode(graph_node['yu_node'])
image_run_node.clear()
# 循环去image_part_list找name和yu_name相等的图片
for img_part in image_part_list:
# 1.如果找到相等
if img_part['name'] == graph_node['yu_name']:
# 2.找到即可添加图片到“域”
image_run_node.clear()
# 辅助:去找其父节点是否为段落,是段落则存起来,后面好居中
image_run_parent_paragraph = image_run_node.getparent()
father_paragraph = None
if isinstance(image_run_parent_paragraph, CT_P):
father_paragraph = Paragraph(image_run_parent_paragraph, doc)
copied_bytes_io = BytesIO(img_part['img'].image.blob)
r_element = Run(image_run_node, doc)
inline_shape = r_element.add_picture(copied_bytes_io)
## 2.1.统一:这里设置文档片段里面的图片大小和位置
source_width = inline_shape.width
source_height = inline_shape.height
if source_width >= source_height:
inline_shape.width = Mm(120)
inline_shape.height = int(inline_shape.height * (inline_shape.width / source_width))
else:
inline_shape.height = Mm(60)
inline_shape.width = int(inline_shape.width * (inline_shape.height / source_height))
## 2.2.设置图片所在段落居中对齐
if father_paragraph:
father_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
r_element.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
# 3.因为按顺序的所以移除image_part_list中已经替换的图片
image_part_list.remove(img_part)
break
try:
# 这里直接生成产品文档
doc.save(str(to_tpl_file))
return to_tpl_file, seitai_final_file
except PermissionError as e:
return {'code': 'error', 'msg': '生成的temp文件已打开请关闭后重试...'}
def get_frag_from_document(doc_path: Path) -> List[Dict]:
"""传入products的文件路径识别出所有文档片段名称数组返回要求docx里面文档名称不能更变"""
doc = Document(doc_path.as_posix())
sdt_element_list = doc.element.body.xpath('./w:sdt')
# 整个for循环识别文档片段名称
area_name_list = []
for sdt_ele in sdt_element_list:
isLock = False
alias_value = None
for elem in sdt_ele.iterchildren():
if elem.tag.endswith('sdtPr'):
for el in elem.getchildren():
if el.tag.endswith('alias'):
alias_value = el.attrib.values()
# 查找是否被用户在模版上标记了Lock
if el.tag.endswith('lock'):
isLock = True
if alias_value and len(alias_value):
area_name_list.append({'frag_name': alias_value[0], 'isLock': isLock})
return area_name_list
# 辅助函数-传入temp文件路径已替换文档片段的temp文档输出stdContent
def get_jinja_stdContent_element(temp_docx_path: Path):
doc_docx = Document(temp_docx_path.as_posix())
body = doc_docx.element.body
# 储存文本片段
text_frag_name_list = []
sdt_element_list = body.xpath('//w:sdt')
# 注意python-docx的页头的文本片段不在body里面而在section.header里面
# 所以定义辅助函数,统一处理
def deel_sdt_content(*args):
"""传入sdt_element列表将其sdtContent加入外部的文本片段列表"""
for sdt_ele in args:
# 找出每个sdt下面的3个标签
tag_value = None
alias_value = None
sdtContent_ele = None
for sdt_ele_child in sdt_ele.iterchildren():
if sdt_ele_child.tag.endswith('sdtPr'):
for sdtPr_ele_child in sdt_ele_child.getchildren():
if sdtPr_ele_child.tag.endswith('tag'):
if len(sdtPr_ele_child.attrib.values()) > 0:
tag_value = sdtPr_ele_child.attrib.values()[0]
if sdtPr_ele_child.tag.endswith('alias'):
if len(sdtPr_ele_child.attrib.values()) > 0:
alias_value = sdtPr_ele_child.attrib.values()[0]
if sdt_ele_child.tag.endswith('sdtContent'):
sdtContent_ele = sdt_ele_child
# 找出所有tag_value为jinja的文本片段
if tag_value == 'jinja' and alias_value is not None and sdtContent_ele is not None:
text_frag_name_list.append({'alias': alias_value, 'sdtContent': sdtContent_ele})
deel_sdt_content(*sdt_element_list)
for section in doc_docx.sections:
header = section.header
header_sdt_list = header.part.element.xpath('//w:sdt')
deel_sdt_content(*header_sdt_list)
return text_frag_name_list, doc_docx
# 封装一个根据alias名称修改stdContent的函数 -> 在接口处理函数中取数据放入函数修改文档
def stdContent_modify(modify_str: str | bool, doc_docx: Document, sdtContent: _Element):
# 正常处理
for ele in sdtContent:
if isinstance(ele, CT_R):
run_ele = Run(ele, doc_docx)
if isinstance(modify_str, bool):
# 如果是True则不修改原来
if modify_str:
break
else:
modify_str = ""
# 有时候会int类型转换一下防止报错
if isinstance(modify_str, int):
modify_str = str(modify_str)
run_ele.text = modify_str
sdtContent.clear()
sdtContent.append(run_ele._element)
break
if isinstance(ele, CT_P):
para_ele = Paragraph(ele, doc_docx)
if isinstance(modify_str, bool):
if modify_str:
break
else:
modify_str = ""
para_ele.clear()
para_ele.text = modify_str
sdtContent.clear()
sdtContent.append(para_ele._element)
break
"""该文件是:替换文档片段然后生成辅助生成最终文档"""
from io import BytesIO
from typing import List, Dict
from pathlib import Path
from docx import Document
from docx.text.paragraph import Paragraph
from docx.table import Table
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.oxml.text.run import CT_R
from docx.oxml.shape import CT_Picture
from docx.parts.image import ImagePart
from docx.text.run import Run
from docx.shared import Mm
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from lxml.etree import _Element
# 路径工具
from utils.path_utils import project_path
### 模块变量:定义常用图片所在区域的宽高
Demand_table_xqms = Mm(134) # 1.测评大纲-测试项里面-需求描述单元格
Timing_diagram_width = Mm(242) # 2.测试记录-时序图
Test_result_width = Mm(78) # 3.测试记录-测试结果
Horizatal_width = Mm(130) # 4.所有文档-页面图片的横向距离(图片宽度预设置)
def getParentRunNode(node):
"""传入oxml节点对象获取其祖先节点的CT_R"""
if isinstance(node, CT_R):
return node
return getParentRunNode(node.getparent())
def generate_temp_doc(doc_type: str, project_id: int, round_num=None, frag_list=None):
""" 该函数参数:
:param frag_list: 储存用户不覆盖的片段列表
:param round_num: 只有回归说明和回归记录有
:param project_id: 项目id
:param doc_type:大纲 sm:说明 jl:记录 bg:报告 hsm:回归测试说明 hjl:回归测试记录,默认路径为dg -> 所以如果传错就生成生成大纲了
:return (to_tpl_file路径, seitai_final_file路径)
"""
if frag_list is None:
frag_list = []
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
project_path_str = project_path(project_id)
# 根据传入需要处理的文档类型,自动获路径
prefix = Path.cwd() / 'media' / project_path_str
template_file: Path = prefix / 'form_template' / 'products' / '测评大纲.docx'
to_tpl_file: Path = prefix / 'temp' / '测评大纲.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测评大纲.docx'
if doc_type == 'sm':
template_file = prefix / 'form_template' / 'products' / '测试说明.docx'
to_tpl_file = prefix / 'temp' / '测试说明.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测试说明.docx'
elif doc_type == 'jl':
template_file = prefix / 'form_template' / 'products' / '测试记录.docx'
to_tpl_file = prefix / 'temp' / '测试记录.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测试记录.docx'
elif doc_type == 'bg':
template_file = prefix / 'form_template' / 'products' / '测评报告.docx'
to_tpl_file = prefix / 'temp' / '测评报告.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '测评报告.docx'
elif doc_type == 'hsm':
# 如果products里面存在“用户上传的第n轮回归测试说明.docx则使用它作为模版”
template_file = prefix / 'form_template' / 'products' / f'{round_num}轮回归测试说明.docx'
if not template_file.exists():
template_file = prefix / 'form_template' / 'products' / '回归测试说明.docx'
to_tpl_file = prefix / 'temp' / f'{round_num}轮回归测试说明.docx'
seitai_final_file: Path = prefix / 'final_seitai' / f'{round_num}轮回归测试说明.docx'
elif doc_type == 'hjl':
# 如果products里面存在“用户上传的第n轮回归测试记录.docx则使用它作为模版”
template_file = prefix / 'form_template' / 'products' / f'{round_num}轮回归测试记录.docx'
if not template_file.exists():
template_file = prefix / 'form_template' / 'products' / '回归测试记录.docx'
to_tpl_file = prefix / 'temp' / f'{round_num}轮回归测试记录.docx'
seitai_final_file: Path = prefix / 'final_seitai' / f'{round_num}轮回归测试记录.docx'
elif doc_type == 'wtd':
template_file = prefix / 'form_template' / 'products' / '问题单.docx'
to_tpl_file = prefix / 'temp' / '问题单.docx'
seitai_final_file: Path = prefix / 'final_seitai' / '问题单.docx'
# 定义找寻被复制文件根路径 - 后续会根据type找子路径
output_files_path = prefix / 'output_dir'
# 这里可能修改,储存大纲里面的文档片段
dg_copied_files = []
# 储存sm/jl/hsm/hjl/bg/wtd的文档片段
exclusive_copied_files = []
# 新储存reuse的文档片段
reuse_files = []
# 将被拷贝文件分别放入不同两个数组
for file in output_files_path.iterdir():
if file.is_file():
if file.suffix == '.docx':
dg_copied_files.append(file)
elif file.is_dir():
# 如果文件夹名称为sm/jl/hsm/hjl/bg/wtd则进入该判断
# 所以要求文件系统文件夹名称必须是sm/jl/hsm/hjl/bg/wtd不然无法生成
if file.stem == doc_type:
for f in file.iterdir():
if f.suffix == '.docx':
exclusive_copied_files.append(f)
for file in (prefix / 'reuse').iterdir():
if file.is_file():
if file.suffix == '.docx':
reuse_files.append(file)
# 找到基础模版的所有std域
doc = Document(template_file.as_posix())
body = doc.element.body
sdt_element_list = body.xpath('./w:sdt')
# 找到sdt域的名称 -> 为了对应output_dir文件 / 储存所有output_dir图片
area_name_list = []
image_part_list = [] # 修改为字典两个字段{ 'name':'测评对象', 'img':ImagePart }
# 筛选片段【二】:用户前端要求不要覆盖的文档片段
frag_is_cover_dict = {item.name: item.isCover for item in frag_list}
# 遍历所有控件 -> 放入area_name_list【这里准备提取公共代码】
for sdt_ele in sdt_element_list:
isLock = False
for elem in sdt_ele.iterchildren():
# 【一】用户设置lock - 下面2个if将需要被替换的(控件名称)存入area_name_list
if elem.tag.endswith('sdtPr'):
for el in elem.getchildren():
if el.tag.endswith('lock'):
isLock = True
if elem.tag.endswith('sdtPr'):
for el in elem.getchildren():
if el.tag.endswith('alias'):
# 筛序【一】取出用户设置lock的文档片段
if len(el.attrib.values()) > 0 and (isLock == False):
area_name = el.attrib.values()[0]
# 筛选【二】:前端用户选择要覆盖的片段
if frag_is_cover_dict.get(area_name):
area_name_list.append(area_name)
# 下面开始替换area_name_list的“域”这时已经被筛选-因为sdtPr和sdtContent是成对出现
if elem.tag.endswith('sdtContent'):
if len(area_name_list) > 0:
# 从第一个片段名称开始取,取到模版的“域”名称
area_pop_name = area_name_list.pop(0)
# 这里先去找media/output_dir/xx下文件然后找media/output下文件
copied_file_path = ""
# 下面if...else是找output_dir下面文件与“域”名称匹配匹配到存入copied_file_path
if doc_type == 'dg':
for file in dg_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
else:
# 如果不是大纲
if round_num is None:
# 如果非回归说明、记录
for file in exclusive_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
# 这里判断是否copied_file_path没取到文件然后遍历reuse下文件
if not copied_file_path:
for file in reuse_files:
if file.stem == area_pop_name:
copied_file_path = file
# 如果上面被复制文件还没找到然后遍历output_dir下文件
if not copied_file_path:
for file in dg_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
else:
# 因为回归的轮次,前面会加 -> 第{round_num}轮
for file in exclusive_copied_files: # 这里多了第{round_num}轮
if file.stem == f"{round_num}{area_pop_name}":
copied_file_path = file
if not copied_file_path:
for file in reuse_files:
if file.stem == area_pop_name:
copied_file_path = file
if not copied_file_path:
for file in dg_copied_files:
if file.stem == area_pop_name:
copied_file_path = file
# 找到文档片段.docx将其数据复制到对应area_name的“域”
if copied_file_path:
doc_copied = Document(copied_file_path)
copied_element_list = []
element_list = doc_copied.element.body.inner_content_elements
for elet in element_list:
if isinstance(elet, CT_P):
copied_element_list.append(Paragraph(elet, doc_copied))
if isinstance(elet, CT_Tbl):
copied_element_list.append(Table(elet, doc_copied))
elem.clear()
for para_copied in copied_element_list:
elem.append(para_copied._element)
# 下面代码就是将图片全部提取到image_part_list以便后续插入注意这时候已经是筛选后的
doc_copied = Document(copied_file_path) # 需要重新获取否则namespace错误
copied_body = doc_copied.element.body
img_node_list = copied_body.xpath('.//pic:pic')
if not img_node_list:
pass
else:
for img_node in img_node_list:
img: CT_Picture = img_node
# 根据节点找到图片的关联id
embed = img.xpath('.//a:blip/@r:embed')[0]
# 这里得到ImagePart -> 马上要给新文档添加
related_part: ImagePart = doc_copied.part.related_parts.get(embed)
if related_part is None:
# 可选:记录警告日志,便于排查哪些文档片段有问题
print(f"警告: 文档片段 '{area_pop_name}' 中的图片引用 {embed} 未找到,已跳过!!!!")
continue
# doc_copied.part.related_parts是一个字典
image_part_list.append({'name': area_pop_name, 'img': related_part})
# 现在是替换后找到替换后文档所有pic:pic并对“域”名称进行识别
graph_node_list = body.xpath('.//pic:pic')
graph_node_list_transform = []
for picNode in graph_node_list:
# 遍历替换后模版的所有pic去找祖先
sdt_node = picNode.xpath('ancestor::w:sdt[1]')[0]
for sdt_node_child in sdt_node.iterchildren():
# 找到sdt下一级的stdPr
if sdt_node_child.tag.endswith('sdtPr'):
for sdtPr_node_child in sdt_node_child.getchildren():
if sdtPr_node_child.tag.endswith('alias'):
yu_name = sdtPr_node_child.attrib.values()[0]
graph_node_list_transform.append({'yu_name': yu_name, 'yu_node': picNode})
for graph_node in graph_node_list_transform:
image_run_node = getParentRunNode(graph_node['yu_node'])
image_run_node.clear()
# 循环去image_part_list找name和yu_name相等的图片
for img_part in image_part_list:
# 1.如果找到相等
if img_part['name'] == graph_node['yu_name']:
# 2.找到即可添加图片到“域”
image_run_node.clear()
# 辅助:去找其父节点是否为段落,是段落则存起来,后面好居中
image_run_parent_paragraph = image_run_node.getparent()
father_paragraph = None
if isinstance(image_run_parent_paragraph, CT_P):
father_paragraph = Paragraph(image_run_parent_paragraph, doc)
copied_bytes_io = BytesIO(img_part['img'].image.blob)
r_element = Run(image_run_node, doc)
inline_shape = r_element.add_picture(copied_bytes_io)
## 2.1.统一:这里设置文档片段里面的图片大小和位置
source_width = inline_shape.width
source_height = inline_shape.height
if source_width >= source_height:
inline_shape.width = Mm(120)
inline_shape.height = int(inline_shape.height * (inline_shape.width / source_width))
else:
inline_shape.height = Mm(60)
inline_shape.width = int(inline_shape.width * (inline_shape.height / source_height))
## 2.2.设置图片所在段落居中对齐
if father_paragraph:
father_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
r_element.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
# 3.因为按顺序的所以移除image_part_list中已经替换的图片
image_part_list.remove(img_part)
break
try:
# 这里直接生成产品文档
doc.save(str(to_tpl_file))
return to_tpl_file, seitai_final_file
except PermissionError as e:
return {'code': 'error', 'msg': '生成的temp文件已打开请关闭后重试...'}
def get_frag_from_document(doc_path: Path) -> List[Dict]:
"""传入products的文件路径识别出所有文档片段名称数组返回要求docx里面文档名称不能更变"""
doc = Document(doc_path.as_posix())
sdt_element_list = doc.element.body.xpath('./w:sdt')
# 整个for循环识别文档片段名称
area_name_list = []
for sdt_ele in sdt_element_list:
isLock = False
alias_value = None
for elem in sdt_ele.iterchildren():
if elem.tag.endswith('sdtPr'):
for el in elem.getchildren():
if el.tag.endswith('alias'):
alias_value = el.attrib.values()
# 查找是否被用户在模版上标记了Lock
if el.tag.endswith('lock'):
isLock = True
if alias_value and len(alias_value):
area_name_list.append({'frag_name': alias_value[0], 'isLock': isLock})
return area_name_list
# 辅助函数-传入temp文件路径已替换文档片段的temp文档输出stdContent
def get_jinja_stdContent_element(temp_docx_path: Path):
doc_docx = Document(temp_docx_path.as_posix())
body = doc_docx.element.body
# 储存文本片段
text_frag_name_list = []
sdt_element_list = body.xpath('//w:sdt')
# 注意python-docx的页头的文本片段不在body里面而在section.header里面
# 所以定义辅助函数,统一处理
def deel_sdt_content(*args):
"""传入sdt_element列表将其sdtContent加入外部的文本片段列表"""
for sdt_ele in args:
# 找出每个sdt下面的3个标签
tag_value = None
alias_value = None
sdtContent_ele = None
for sdt_ele_child in sdt_ele.iterchildren():
if sdt_ele_child.tag.endswith('sdtPr'):
for sdtPr_ele_child in sdt_ele_child.getchildren():
if sdtPr_ele_child.tag.endswith('tag'):
if len(sdtPr_ele_child.attrib.values()) > 0:
tag_value = sdtPr_ele_child.attrib.values()[0]
if sdtPr_ele_child.tag.endswith('alias'):
if len(sdtPr_ele_child.attrib.values()) > 0:
alias_value = sdtPr_ele_child.attrib.values()[0]
if sdt_ele_child.tag.endswith('sdtContent'):
sdtContent_ele = sdt_ele_child
# 找出所有tag_value为jinja的文本片段
if tag_value == 'jinja' and alias_value is not None and sdtContent_ele is not None:
text_frag_name_list.append({'alias': alias_value, 'sdtContent': sdtContent_ele})
deel_sdt_content(*sdt_element_list)
for section in doc_docx.sections:
header = section.header
header_sdt_list = header.part.element.xpath('//w:sdt')
deel_sdt_content(*header_sdt_list)
return text_frag_name_list, doc_docx
# 封装一个根据alias名称修改stdContent的函数 -> 在接口处理函数中取数据放入函数修改文档
def stdContent_modify(modify_str: str | bool, doc_docx: Document, sdtContent: _Element):
# 正常处理
for ele in sdtContent:
if isinstance(ele, CT_R):
run_ele = Run(ele, doc_docx)
if isinstance(modify_str, bool):
# 如果是True则不修改原来
if modify_str:
break
else:
modify_str = ""
# 有时候会int类型转换一下防止报错
if isinstance(modify_str, int):
modify_str = str(modify_str)
run_ele.text = modify_str
sdtContent.clear()
sdtContent.append(run_ele._element)
break
if isinstance(ele, CT_P):
para_ele = Paragraph(ele, doc_docx)
if isinstance(modify_str, bool):
if modify_str:
break
else:
modify_str = ""
para_ele.clear()
para_ele.text = modify_str
sdtContent.clear()
sdtContent.append(para_ele._element)
break

View File

@@ -1,13 +1,87 @@
from datetime import date
from ninja_extra import api_controller, ControllerBase, route
import json
from apps.project.models import Project
from django.db import transaction
from django.contrib.auth import get_user_model
from utils.chen_response import ChenResponse
from django.db.models import Q
from ninja import Schema
Users = get_user_model()
class AIPostSchema(Schema):
question: str
stream: bool
# AI测试接口
@api_controller("/local_doc_qa", tags=['AI测试接口'])
class AITestController(ControllerBase):
"""AI测试接口自定义延迟"""
@route.post("/testing_item")
def ai_return(self, item: AIPostSchema):
import time
time.sleep(2)
res = [
{
"demandDescription": "验证外部32MHz品振时钟和内部10KHZ时钟能否正确布线至FPGA内部相应的全局时钟网络并通过指定缓冲器降低延迟。",
"title": "时钟布线与缓冲功能测试",
"children": [
{
"name": "外部32MHz时钟布线到HCLKBUF级冲测试",
"subDescription": "验证外部32MH布线的测试子项描述",
"subStep": [
{
"operation": "配置FPGA逻辑将外部32MHz晶振输入连接到HCLKBUF缓冲器。",
"expect": "时钟信号成功接入HCLKBUF缓冲器无错误提示。"
}, {
"operation": "使用示波器或时序分析工具检测HCLKBUF输出端的时钟波形。",
"expect": "输出端应稳定输出32MHz时钟信号频率准确目波形无明显失真。"
}, {
"operation": "监测从HCLKBUF到各寄存器的时钟路径延迟。",
"expect": "各路径延迟保持一致目为最小值,满足分布式延迟最低的变求。"
}
]
}, {
"name": "内部10KHz时钟布线到CLKINT缓冲测试",
"subDescription": "验证内部10KHz时钟布线到CLKINT缓冲的测试子项描述",
"subStep": [
{
"operation": "在FPGA中启用内部10KHz时钟源并将其连接至CLKINT缓冲器。",
"expect": "内部时钟信号成功接入CLKINT缓冲器系统无报错。"
}, {
"operation": "测量CLKINT输出端的时钟频率。",
"expect": "输出端应稳定输出10KHz时钟信号频率精度符合设计要求。"
}, {
"operation": "检查CLKINT是否将时钟广播到全局时钟网器",
"expect": "时钟能被正常分发至内部各个需要该时钟的模块。"
}
]
}, {
"name": "异常情况下的时钟处理测试",
"subDescription": "验证异常情况下的时钟处理测试的测试子项描述",
"subStep": [
{
"operation": "断开外部32MHz晶振输入后尝试进行HCLKBUF配置。",
"expect": "系统应报告时钟缺失错误,无法完成正常的时钟分配。"
}, {
"operation": "人为制造内部10KHz时钟不稳定(如干扰)后再送入CLKINT。",
"expect": "CLKINT应拒绝不稳定的时钟或将错误上报给监控机制。"
}, {
"operation": "同时配置两个时钟但未正确绑定各自缓冲器。",
"expect": "系统应阻止非法配置操作,确保每个时钟进入正确的缓冲通道。"
}
]
}
]
}
]
return {
"history": [["我是没有用的", json.dumps(res)]]
}
# 这是其他common内容接口
@api_controller("/system", tags=['通用接口'])
class CommonController(ControllerBase):
@@ -19,7 +93,8 @@ class CommonController(ControllerBase):
item1 = {"title": "测试管理平台V0.0.2测试发布", "created_at": "2023-09-23",
"content": "测试管理平台V0.0.2发布,正在进行内部测试.."}
item_list.append(item1)
item2 = {"title": "测试管理平台更新公共", "created_at": "2024-06-17", "content": "<p>1.修改大纲和报告模版<p><p>2.修复多个bug<p>"}
item2 = {"title": "测试管理平台更新公共", "created_at": "2024-06-17",
"content": "<p>1.修改大纲和报告模版<p><p>2.修复多个bug<p>"}
item_list.append(item2)
return item_list

View File

@@ -8,8 +8,11 @@ from utils.chen_pagination import MyPagination
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.db.models.functions import Replace
from django.db.models import Q, F, Value
from django.db.models import F, Value
from typing import List
from faker import Faker
from datetime import datetime
from django.utils import timezone
from utils.chen_response import ChenResponse
from utils.chen_crud import multi_delete_case
from apps.project.models import Design, Dut, Round, TestDemand, Case, CaseStep, Project, Problem
@@ -20,8 +23,9 @@ from utils.util import get_testType
from utils.codes import HTTP_INDEX_ERROR, HTTP_EXISTS_CASES
from apps.project.tools.copyCase import case_move_to_test, case_copy_to_test, case_to_case_copy_or_move
from utils.smallTools.interfaceTools import conditionNoneToBlank
from apps.project.tool.batchTools import parse_case_content_string
# 导入case的schema
from apps.project.schemas.case import CaseModelOutSchemaWithoutProblem
from apps.project.schemas.case import CaseModelOutSchemaWithoutProblem, BatchCreateCaseInputSchema
@api_controller("/project", auth=JWTAuth(), permissions=[IsAuthenticated], tags=['测试用例接口'])
class CaseController(ControllerBase):
@@ -120,7 +124,7 @@ class CaseController(ControllerBase):
@transaction.atomic
def create_case(self, payload: CaseCreateInputSchema):
asert_dict = payload.dict(exclude_none=True)
# 构造design_key
# 构造demand_key
test_whole_key = "".join(
[payload.round_key, "-", payload.dut_key, '-', payload.design_key, '-', payload.test_key])
# 查询当前key应该为多少
@@ -157,6 +161,52 @@ class CaseController(ControllerBase):
CaseStep.objects.bulk_create(data_list) # type:ignore
return qs
# 批量新增用例
@route.post("/case/multi_save", url_name="case-batch-create")
@transaction.atomic
def multi_case_save(self, payload: BatchCreateCaseInputSchema):
project_obj = get_object_or_404(Project, id=payload.project_id)
user_name = self.context.request.user.name
keys = []
demands = project_obj.ptField.all() # 当前项目所有测试项
for case_data in payload.cases:
# 解析放在前面防止出错
stepsOrErrorResponse = parse_case_content_string(case_data.test_step)
if isinstance(stepsOrErrorResponse, ChenResponse):
return stepsOrErrorResponse
# 查询当前测试项下case数量以设置case的key
demand_key = case_data.parent_key
demand_obj = demands.filter(key=demand_key).first()
case_count = demand_obj.tcField.count()
key_string = ''.join([demand_key, "-", str(case_count)])
keys.append(key_string)
case_dict = {
"ident": demand_obj.ident,
"name": case_data.name,
"key": key_string,
"initialization": case_data.initialization,
"premise": case_data.premise,
"summarize": case_data.summarize,
"designPerson": user_name,
"testPerson": user_name,
"monitorPerson": user_name,
"project": project_obj,
"round": demand_obj.round,
"dut": demand_obj.dut,
"design": demand_obj.design,
"test": demand_obj,
"exe_time": timezone.now(),
"timing_diagram": case_data.sequence,
"title": case_data.name
}
case_new_obj = Case.objects.create(**case_dict)
case_step_list = []
for step in stepsOrErrorResponse:
case_step_list.append(CaseStep(**{"case": case_new_obj, "operation": step['operation'],
"expect": step['expect']}))
CaseStep.objects.bulk_create(case_step_list)
return ChenResponse(code=60000, status=200, data=keys, message='成功录入用例')
# 更新测试用例
@route.put("/case/update/{id}", response=CaseCreateOutSchema, url_name="case-update")
@transaction.atomic
@@ -207,7 +257,7 @@ class CaseController(ControllerBase):
single_qs.key = case_key
index = index + 1
single_qs.save()
return ChenResponse(message="测试用例删除成功!")
return ChenResponse(message="测试用例删除成功!影响域分析中如果有该关联用例则被删除。")
# 右键测试项,根据测试子项生成用例
@route.post("/case/create_by_demand", url_name='case-create-by-demand')
@@ -301,7 +351,8 @@ class CaseController(ControllerBase):
# 批量更新 operation 和 expect
step_count = caseStep_qs.update(
operation=Replace(F('operation'), Value(payload.originText), Value(payload.replaceText)),
expect=Replace(F('expect'), Value(payload.originText), Value(payload.replaceText))
expect=Replace(F('expect'), Value(payload.originText), Value(payload.replaceText)),
result=Replace(F('result'), Value(payload.originText), Value(payload.replaceText))
)
# 5.提交更新
replace_count = case_qs.update(**replace_kwargs)
@@ -324,6 +375,42 @@ class CaseController(ControllerBase):
@route.post("/case/timeReplace/", url_name='case-time-replace')
@transaction.atomic
def bulk_replace_time(self, payload: ExetimeReplaceSchema):
selected_case_ids = payload.selectRows
if not selected_case_ids:
return ChenResponse(status=500, code=50999, message='未选择行!', data="")
# 随机日期
start, end = payload.exetime
start_date = datetime.strptime(start, "%Y-%m-%d").date()
end_date = datetime.strptime(end, "%Y-%m-%d").date()
# 更新的case的id列表
updated_cases = []
# 替换设计人员
case_qs = Case.objects.filter(id__in=payload.selectRows)
case_qs.update(exe_time=payload.exetime)
faker = Faker()
# 逐个更新
for case in case_qs:
random_date = faker.date_between(start_date=start_date, end_date=end_date)
formatted_date = random_date.strftime("%Y-%m-%d")
case.exe_time = formatted_date
updated_cases.append(case)
Case.objects.bulk_update(updated_cases, ['exe_time'])
return ChenResponse(status=200, code=200, data=len(updated_cases),
message=f"成功更新{len(updated_cases)}个用例执行时间")
# 给级联选择器数据 -> 上一轮次所有用例
@route.get("/case/getRelatedCase", url_name='case-related-case')
def get_cases_related_case(self, id: int, round_key: str):
project_obj = get_object_or_404(Project, id=id)
previous_round_obj = project_obj.pField.filter(key=int(round_key) - 1).first()
# dut -> design
data_list = []
for dut in previous_round_obj.rdField.all():
dut_dict = {'label': dut.name, 'value': dut.id, 'key': dut.key, 'children': []}
for design in dut.rsField.all():
design_dict = {'label': design.name, 'value': design.id, 'key': design.key, 'children': []}
for case in design.dcField.all():
case_dict = {'label': case.name, 'value': case.id, 'key': case.key}
design_dict['children'].append(case_dict)
dut_dict['children'].append(design_dict)
data_list.append(dut_dict)
return ChenResponse(message='获取成功', data=data_list)

View File

@@ -1,3 +1,5 @@
import re
from copy import deepcopy
from ninja_extra import api_controller, ControllerBase, route
from ninja import Query
from ninja_jwt.authentication import JWTAuth
@@ -13,13 +15,31 @@ from typing import List
from utils.chen_response import ChenResponse
from utils.chen_crud import multi_delete_design
from utils.codes import HTTP_INDEX_ERROR
from apps.project.models import Design, Dut, Round, Project
from apps.project.models import Design, Dut, Round, Project, JKDesignInfo
from apps.project.schemas.design import DeleteSchema, DesignFilterSchema, DesignModelOutSchema, \
DesignTreeReturnSchema, \
DesignTreeInputSchema, DesignCreateOutSchema, DesignCreateInputSchema, MultiDesignCreateInputSchema, \
ReplaceDesignContentSchema
from apps.project.tools.delete_change_key import design_delete_sub_node_key
from utils.smallTools.interfaceTools import conditionNoneToBlank
from apps.project.tools.auto_create_data import auto_create_renji
from apps.project.tool.dragAndDrop import DesignDrapAtoB
def _save_jk_direction_info(design: Design, direction: str, source: str, destination: str, description: str):
"""保存或更新 JKDesignInfo 记录"""
if not source and not destination and not description:
# 如果三个字段全为空,则删除可能存在的记录(避免冗余数据)
JKDesignInfo.objects.filter(jk=design, direction=direction).delete()
return
JKDesignInfo.objects.update_or_create(
jk=design,
direction=direction,
defaults={
'source': source or '',
'destination': destination or '',
'description': description or '',
}
)
@api_controller("/project", auth=JWTAuth(), permissions=[IsAuthenticated], tags=['设计需求数据'])
class DesignController(ControllerBase):
@@ -63,7 +83,10 @@ class DesignController(ControllerBase):
# 处理树状数据
@route.get("/getDesignDemandInfo", response=List[DesignTreeReturnSchema], url_name="design-info")
def get_design_tree(self, payload: DesignTreeInputSchema = Query(...)):
qs = Design.objects.filter(project__id=payload.project_id, dut__key=payload.key).order_by('id')
qs = Design.objects.filter(
project__id=payload.project_id,
dut__key=payload.key
).select_related('project', 'dut')
return qs
# 添加设计需求
@@ -90,7 +113,30 @@ class DesignController(ControllerBase):
{'key': key_string, 'round': round_instance, 'dut': dut_instance, 'title': payload.name})
asert_dict.pop("round_key")
asert_dict.pop("dut_key")
# 去掉Design不使用的字段
asert_dict.pop("forward_source", None)
asert_dict.pop("forward_destination", None)
asert_dict.pop("forward_description", None)
asert_dict.pop("reverse_source", None)
asert_dict.pop("reverse_destination", None)
asert_dict.pop("reverse_description", None)
qs = Design.objects.create(**asert_dict)
# 处理接口方向信息(仅当 demandType == '3' 且存在正向/反向数据时)
if payload.demandType == '3':
_save_jk_direction_info(
design=qs,
direction=JKDesignInfo.Direction.FORWARD,
source=payload.forward_source,
destination=payload.forward_destination,
description=payload.forward_description,
)
_save_jk_direction_info(
design=qs,
direction=JKDesignInfo.Direction.REVERSE,
source=payload.reverse_source,
destination=payload.reverse_destination,
description=payload.reverse_description,
)
return qs
# 批量增加设计需求对应前端批量增加页面modal
@@ -129,15 +175,36 @@ class DesignController(ControllerBase):
# 判断是否和同项目同轮次的标识重复
if len(design_search) > 1 and payload.ident != '':
return ChenResponse(code=400, status=400, message='研制需求的标识重复,请检查')
# 查到当前
design_qs = Design.objects.get(id=id)
for attr, value in payload.dict().items():
if attr == 'project_id' or attr == 'round_key' or attr == 'dut_key':
if attr in ('project_id', 'round_key', 'dut_key'):
continue
if attr == 'name':
setattr(design_qs, "title", value)
setattr(design_qs, attr, value)
design_qs.save()
# 处理接口方向信息更新
if payload.demandType == '3':
_save_jk_direction_info(
design=design_qs,
direction=JKDesignInfo.Direction.FORWARD,
source=payload.forward_source,
destination=payload.forward_destination,
description=payload.forward_description,
)
_save_jk_direction_info(
design=design_qs,
direction=JKDesignInfo.Direction.REVERSE,
source=payload.reverse_source,
destination=payload.reverse_destination,
description=payload.reverse_description,
)
else:
# 如果需求类型不再是接口,则删除已有的方向信息
JKDesignInfo.objects.filter(jk=design_qs).delete()
return design_qs
# 删除设计需求
@@ -162,7 +229,7 @@ class DesignController(ControllerBase):
design_delete_sub_node_key(single_qs)
return ChenResponse(message="设计需求删除成功!")
# 给复制功能级联选择器查询所有的设计需求
# 给复制功能级联选择器查询所有的设计需求【这是查项目所有的设计需求】
@route.get("/designDemand/getRelatedDesign", url_name='dut-relatedDesign')
def getRelatedDesign(self, id: int):
project_qs = get_object_or_404(Project, id=id)
@@ -196,3 +263,66 @@ class DesignController(ControllerBase):
# 4.提交更新
replace_count = design_qs.update(**replace_kwargs)
return {'count': replace_count}
# 点击生成人机交互界面测试-注意必须要有界面的软件
@route.get("/create_renji/", url_name='renji')
@transaction.atomic
def create_rj(self, round_id: int, project_id: int):
user_name = self.context.request.user.name # 获取当前用户名
project_obj: Project = get_object_or_404(Project, id=project_id)
dut_qs = Dut.objects.filter(round__key=round_id, project=project_obj, type='XQ').first()
if dut_qs:
auto_create_renji(user_name, dut_qs, project_obj)
return ChenResponse(status=200, message='自动生成人机界面交互测试成功!', data=dut_qs.key)
return ChenResponse(status=402, message='您还未录入需求规格说明文档,请录入后再试')
# 复制design到当前dut下面接口
@route.get("/copy_current", url_name='copy-design-current')
@transaction.atomic
def copy_current(self, dut_id: int, design_id: int):
dut_obj = get_object_or_404(Dut, id=dut_id)
design_obj = get_object_or_404(Design, id=design_id)
# 首先查询该dut下design个数设置为新增设计需求的key末尾
key_index = dut_obj.rsField.count()
new_design_obj = deepcopy(design_obj)
# 修改新design内容
new_design_obj.pk = None
new_design_obj.key = "".join([dut_obj.key, "-", str(key_index)])
new_design_obj.title = "".join([design_obj.title, "(复制)"])
new_design_obj.name = "".join([design_obj.name, "(复制)"])
# ident容错查询是否有拼接的
current_ident = "".join([new_design_obj.ident, "1"])
project_obj = dut_obj.project
exit_ident = project_obj.psField.filter(ident=current_ident).exists()
if exit_ident:
match = re.search(r'(\d+)$', current_ident)
if match:
num = int(match.group(1)) + 1
current_ident = re.sub(r'\d+$', str(num), current_ident)
else:
current_ident = current_ident + "1"
new_design_obj.ident = current_ident
# 最后记得save
new_design_obj.save()
return ChenResponse(status=200, code=200, message='复制当前设计需求成功', data="")
# 拖拽更变desing的key同dut下其他design也变动
@route.get("/switch_position", url_name='design-switch-position')
@transaction.atomic
def switch_position(self, from_key: str, to_key: str, pos: int, project_id: int):
from_key_list = from_key.split("-")
to_key_list = to_key.split("-")
# 如果两个设计需求被测件或轮次不一样则报错
if from_key_list[:-1] != to_key_list[:-1]:
return ChenResponse(status=422, code=40022, message="无法交换不同父节点的设计需求")
# 先查询两个design
from_design_obj: Design = Design.objects.filter(key=from_key, project_id=project_id).first()
to_design_obj: Design = Design.objects.filter(key=to_key, project_id=project_id).first()
if not from_design_obj or not to_design_obj:
return ChenResponse(status=404, code=40004, message="设计需求不存在")
# 获取父节点下所有design
parant_dut = from_design_obj.dut
design_qs = parant_dut.rsField.all()
# 根据pos将from排到后面
return_key = DesignDrapAtoB(from_design_obj, to_design_obj, design_qs, pos)
return ChenResponse(status=200, data=return_key)

View File

@@ -35,19 +35,25 @@ class ProblemController(ControllerBase):
def get_problem_list(self, data: ProblemFilterSchema = Query(...)):
project_id = data.project_id
conditionNoneToBlank(data)
case_key = "".join([data.round_id, '-', data.dut_id, '-', data.design_id, '-', data.test_id, '-', data.case_id])
# 先查询出对应的case
case_obj = Case.objects.filter(project_id=project_id, key=case_key).first()
# 然后进行过滤
qs = case_obj.caseField.filter(project__id=data.project_id,
ident__icontains=data.ident,
name__icontains=data.name,
status__icontains=data.status,
type__icontains=data.type,
grade__icontains=data.grade,
operation__icontains=data.operation,
postPerson__icontains=data.postPerson,
).order_by("id")
# 组装查询条件
query_params = {
"project__id":data.project_id,
"ident__icontains":data.ident,
"name__icontains":data.name,
"status__icontains":data.status,
"type__icontains":data.type,
"grade__icontains":data.grade,
"operation__icontains":data.operation,
"postPerson__icontains":data.postPerson
}
# 如果没有多个key传递则是汇总界面
if data.dut_id and data.design_id and data.test_id and data.case_id:
case_key = "".join(
[data.round_id, '-', data.dut_id, '-', data.design_id, '-', data.test_id, '-', data.case_id])
query_params['case__key'] = case_key
else:
query_params['case__round__key'] = data.round_id
qs = Problem.objects.filter(**query_params).order_by("id")
# 遍历通过代码不通过ORM查询闭环方式-巧妙使用numpy中array对象的in方法来判断
closeMethod1 = self.context.request.GET.get("closeMethod[0]")
@@ -73,9 +79,7 @@ class ProblemController(ControllerBase):
@paginate(MyPagination)
def get_all_problems(self, round_key: Optional[str] = False, data: ProblemFilterWithHangSchema = Query(...)):
project_id = data.project_id
for attr, value in data.__dict__.items():
if getattr(data, attr) is None:
setattr(data, attr, '')
conditionNoneToBlank(data)
# 先查询当前项目
qs = Problem.objects.filter(project__id=data.project_id,
ident__icontains=data.ident,

View File

@@ -10,11 +10,14 @@ from ninja_jwt.authentication import JWTAuth
from apps.user.models import Users
from utils.chen_pagination import MyPagination
from ninja.pagination import paginate
from ninja.errors import HttpError
from ninja import Query
from utils.chen_response import ChenResponse
from utils.chen_crud import create, multi_delete_project
from apps.project.models import Project, Round
from apps.project.schemas.project import ProjectRetrieveSchema, ProjectFilterSchema, ProjectCreateInput, DeleteSchema
from apps.project.models import Project, Round, ProjectSoftSummary, StuctSortData, StaticSoftItem, StaticSoftHardware, DynamicSoftTable, \
DynamicHardwareTable, ProjectDynamicDescription, EvaluateData, EnvAnalysis
from apps.project.schemas.project import ProjectRetrieveSchema, ProjectFilterSchema, ProjectCreateInput, \
DeleteSchema, SoftSummarySchema, DataSchema, StaticDynamicData, EnvAnalysisSchema
from utils.util import get_str_dict
# 时间处理模块
from apps.project.tool.timeList import time_return_to
@@ -89,12 +92,14 @@ class ProjectController(ControllerBase):
try:
copytree(src_dir, dist_dir) # shutil模块直接是复制并命名如果命名文件存在则抛出FileExists异常
except PermissionError:
return ChenResponse(code=500, status=500, message="错误检查是否打开了服务器的conf中的文档关闭后重试")
return ChenResponse(code=500, status=500,
message="错误检查是否打开了服务器的conf中的文档关闭后重试")
except FileExistsError:
return ChenResponse(code=500, status=500, message='文件标识已存在或输入为空格,请修改')
except FileNotFoundError:
return ChenResponse(code=500, status=500, message='文件不存在,请检查')
return ChenResponse(code=200, status=200, message="添加项目成功,并添加第一轮测试")
return ChenResponse(code=400, status=400, message="未添加任何项目")
@route.put("/update/{project_id}")
@transaction.atomic
@@ -135,7 +140,7 @@ class ProjectController(ControllerBase):
project_media_path = media_path / ident
try:
rmtree(project_media_path)
except FileNotFoundError as e:
except FileNotFoundError:
return ChenResponse(status=400, code=400, message='项目模版目录可能不存在,可能之前已删除')
return ChenResponse(message="删除成功!")
@@ -183,8 +188,8 @@ class ProjectController(ControllerBase):
# 7.将时间提取 todo:后续将计算的事件放入该页面
timers = {'round_time': []}
rounds = project_obj.pField.all()
timers['start_time'] = project_obj.beginTime
timers['end_time'] = project_obj.endTime
timers['start_time'] = project_obj.beginTime # type:ignore
timers['end_time'] = project_obj.endTime # type:ignore
for round in rounds:
round_number = int(round.key) + 1
timers['round_time'].append({
@@ -197,7 +202,8 @@ class ProjectController(ControllerBase):
# 9.提取测试类型下面测试项数量、用例数量
data_list = []
for round in rounds:
round_dict = {'name': f'{int(round.key) + 1}轮次', 'desings': [], 'method_demand': {}, 'method_case': {}}
round_dict = {'name': f'{int(round.key) + 1}轮次', 'desings': [], 'method_demand': {},
'method_case': {}}
designs = round.dsField.all()
for design in designs:
design_dict = {
@@ -264,3 +270,228 @@ class ProjectController(ControllerBase):
def document_time_show(self, id: int):
time = time_return_to(id)
return time
# [变] 项目级信息前端告警数据获取
@route.get("/project_info_status/")
@transaction.atomic
def project_info_status(self, id: int):
project_obj = self.get_project_by_id(id)
# 统一配置每个状态的检查逻辑
status_configs = {
"soft_summary": {
"model": ProjectSoftSummary,
"check": lambda qs: qs.exists() and qs.first().data_schemas.exists()
},
"interface_image": {
"model": StuctSortData,
"check": lambda qs: qs.exists()
},
"static_soft_item": {
"model": StaticSoftItem,
"check": lambda qs: qs.exists()
},
"static_soft_hardware": {
"model": StaticSoftHardware,
"check": lambda qs: qs.exists()
},
"dynamic_soft_item": {
"model": DynamicSoftTable,
"check": lambda qs: qs.exists()
},
"dynamic_soft_hardware": {
"model": DynamicHardwareTable,
"check": lambda qs: qs.exists()
},
"dynamic_des": {
"model": ProjectDynamicDescription,
"check": lambda qs: qs.exists() and qs.first().data_schemas.exists()
},
"evaluate_data": {
"model": EvaluateData,
"check": lambda qs: qs.exists()
},
"env_analysis": {
"model": EnvAnalysis,
"check": lambda qs: qs.exists()
}
}
all_status = {}
for status_key, config in status_configs.items():
qs = config["model"].objects.filter(project=project_obj)
all_status[status_key] = config["check"](qs)
return ChenResponse(status=200, code=20000, data=all_status, message='查询成功')
# [变] 封装结构化数据新增-修改针对project - OneToOne - DataSchemas形式
@classmethod
def bulk_create_data_schemas(cls, parent_obj, datas: list[DataSchema]):
"""
批量创建结构化排序数据 (自动类型推断)
Args:
parent_obj: 父级对象,可以是 ProjectSoftSummary 或 Project 的实例
datas (list[DataSchema]): 数据模式对象列表
"""
# 动态确定所属父model
field_name = None # type:ignore
if isinstance(parent_obj, ProjectSoftSummary):
field_name = 'soft_summary'
elif isinstance(parent_obj, Project):
field_name = 'project'
elif isinstance(parent_obj, ProjectDynamicDescription):
field_name = 'dynamic_description'
else:
raise HttpError(400, "添加的数据未在系统内,请联系管理员")
data_list = []
for data in datas:
new_data = StuctSortData(
type=data.type,
fontnote=data.fontnote,
content=data.content,
)
setattr(new_data, field_name, parent_obj)
data_list.append(new_data)
StuctSortData.objects.bulk_create(data_list)
# 封装只有model不同 -修改和新增dataSchemas针对project - OneToOne - DataSchemas形式
@classmethod
def create_or_modify_data_schemas(cls, id: int, model, data):
project_obj = get_object_or_404(Project, pk=id)
qs = model.objects.filter(project=project_obj)
if qs.exists():
obj = qs.first()
# 如果存在则修改:先删除再创建
obj.data_schemas.all().delete()
cls.bulk_create_data_schemas(obj, data)
else:
parent_obj = model.objects.create(project=project_obj)
cls.bulk_create_data_schemas(parent_obj, data)
# ~~~软件概述-新增和修改~~~
@route.post('/soft_summary/')
@transaction.atomic
def soft_summary(self, payload: SoftSummarySchema):
self.create_or_modify_data_schemas(payload.id, ProjectSoftSummary, payload.data)
# ~~~动态环境描述-新增和修改~~~
@route.post('/dynamic_description/')
@transaction.atomic
def dynamic_description(self, payload: SoftSummarySchema):
self.create_or_modify_data_schemas(payload.id, ProjectDynamicDescription, payload.data)
@classmethod
def get_res_from_info(cls, project_obj: Project, model) -> list[dict] | None:
"""model: 当前一对一模型,直接获取结构化数据信息数组返回"""
qs = model.objects.filter(project=project_obj)
if qs.exists():
obj = qs.first()
ds_qs = obj.data_schemas.all()
data_list = [{
"type": item.type,
"content": item.content,
"fontnote": item.fontnote,
} for item in ds_qs]
return data_list
return None
# ~~~软件概述-获取到前端展示~~~
@route.get("/get_soft_summary/", response=list[DataSchema])
@transaction.atomic
def get_soft_summary(self, id: int):
project_obj = self.get_project_by_id(id)
data_list = self.get_res_from_info(project_obj, ProjectSoftSummary)
if data_list:
return ChenResponse(status=200, code=20000, data=data_list)
return ChenResponse(status=200, code=20000, data=[])
# ~~~动态环境描述 - 获取展示~~~
@route.get("/dynamic_des/", response=list[DataSchema])
@transaction.atomic
def get_dynamic_des(self, id: int):
project_obj = self.get_project_by_id(id)
data_list = self.get_res_from_info(project_obj, ProjectDynamicDescription)
if data_list:
return ChenResponse(status=200, code=20000, data=data_list)
return ChenResponse(status=200, code=20000, data=[])
# ~~~接口图新增或修改~~~
@route.post("/interface_image/")
@transaction.atomic
def post_interface_image(self, id: int, dataSchema: DataSchema):
project_obj = self.get_project_by_id(id)
image_qs = StuctSortData.objects.filter(project=project_obj)
if image_qs.exists():
image_qs.delete()
self.bulk_create_data_schemas(project_obj, [dataSchema])
# ~~~接口图-获取数据~~~
@route.get("/get_interface_image/", response=DataSchema)
@transaction.atomic
def get_interface_image(self, id: int):
project_obj = self.get_project_by_id(id)
image_qs = StuctSortData.objects.filter(project=project_obj)
if image_qs.exists():
# 如果存在则返回数据
image_obj = image_qs.first()
return ChenResponse(status=200, code=25001, data={
"type": image_obj.type,
"content": image_obj.content,
"fontnote": image_obj.fontnote,
})
return ChenResponse(status=200, code=25002, data=None)
# 动态返回是哪个模型
@classmethod
def get_model_from_category(cls, category: str):
mapDict = {
'静态软件项': StaticSoftItem,
'静态硬件项': StaticSoftHardware,
'动态软件项': DynamicSoftTable,
'动态硬件项': DynamicHardwareTable,
'测评数据': EvaluateData
}
return mapDict[category]
# ~~~静态软件项、静态硬件项、动态软件项、动态硬件项 - 获取~~~
@route.get("/get_static_dynamic_items/")
def get_static_dynamic_items(self, id: int, category: str):
project_obj = self.get_project_by_id(id)
item_qs = self.get_model_from_category(category).objects.filter(project=project_obj)
if item_qs.exists():
item_obj = item_qs.first()
return ChenResponse(status=200, code=25001, data={"table": item_obj.table, "fontnote": item_obj.fontnote})
return ChenResponse(status=200, code=25002, data=None)
# ~~~静态软件项、静态硬件项、动态软件项、动态硬件项、测评数据 - 新增或修改~~~
@route.post("/post_static_dynamic_item/")
@transaction.atomic
def post_static_dynamic_item(self, data: StaticDynamicData):
project_obj = self.get_project_by_id(data.id)
model = self.get_model_from_category(data.category)
item_qs = model.objects.filter(project=project_obj)
if item_qs.exists():
# 如果存在则修改
item_qs.delete()
model.objects.create(project=project_obj, table=data.table, fontnote=data.fontnote)
# ~~~环境差异性分析 - 获取~~~
@route.get("/get_env_analysis/")
@transaction.atomic
def get_env_analysis(self, id: int):
project_obj = self.get_project_by_id(id)
qs = EnvAnalysis.objects.filter(project=project_obj)
if qs.exists():
obj = qs.first()
return ChenResponse(status=200, code=25001, data={"table": obj.table, "fontnote": obj.fontnote, "description": obj.description})
return ChenResponse(status=200, code=25002, data=None)
# ~~~环境差异性分析 - 新增和修改~~~
@route.post("/post_env_analysis/")
@transaction.atomic
def post_env_analysis(self, data: EnvAnalysisSchema):
project_obj = self.get_project_by_id(data.id)
qs = EnvAnalysis.objects.filter(project=project_obj)
if qs.exists():
qs.delete()
EnvAnalysis.objects.create(project=project_obj, table=data.table, fontnote=data.fontnote, description=data.description)

View File

@@ -2,9 +2,9 @@ from ninja_extra import api_controller, ControllerBase, route
from ninja_jwt.authentication import JWTAuth
from ninja_extra.permissions import IsAuthenticated
from django.db import transaction
from apps.project.models import Round
from apps.project.models import Round, InfluenceArea, InfluenceItem
from apps.project.schemas.round import TreeReturnRound, RoundInfoOutSchema, EditSchemaIn, DeleteSchema, \
CreateRoundOutSchema, CreateRoundInputSchema
CreateRoundOutSchema, CreateRoundInputSchema, InfluenceItemOutSchema, InfluenceInputSchema
from typing import List
from utils.chen_response import ChenResponse
from apps.project.tools.delete_change_key import round_delete_sub_node_key
@@ -81,3 +81,62 @@ class RoundController(ControllerBase):
return ChenResponse(code=400, status=400, message='标识和其他重复')
Round.objects.create(**asert_dict)
return ChenResponse(message="新增轮次成功")
# ~~~影响域分析 - 获取数据和状态~~~
@route.get("/round/get_influence", response=List[InfluenceItemOutSchema], url_name="round-get-influence-items")
@transaction.atomic
def get_influence(self, id: int, round_key: str):
round_qs = Round.objects.filter(project__id=id, key=round_key)
round_obj = round_qs.first()
influence_qs = InfluenceArea.objects.filter(round=round_obj)
if influence_qs.exists():
influence = influence_qs.first()
items_qs = influence.influence_items.all()
if items_qs.exists():
return items_qs
return ChenResponse(status=200, code=25002, data=[])
# ~~~影响域分析是否有值~~~
@route.get("/round/get_status_influence", url_name="round-get-status-influence")
@transaction.atomic
def get_status_influence(self, id: int, round_key: str):
round_qs = Round.objects.filter(project__id=id, key=round_key)
round_obj = round_qs.first()
influence_qs = InfluenceArea.objects.filter(round=round_obj)
if influence_qs.exists():
influence = influence_qs.first()
items_qs = influence.influence_items.all()
if items_qs.exists():
return ChenResponse(status=200, code=25005, data=True)
return ChenResponse(status=200, code=25006, data=False)
# ~~~影响域分析 - 修改或新增~~~
@route.post("/round/create_influence", url_name="round-influence-create")
@transaction.atomic
def post_influence(self, data: InfluenceInputSchema):
round_obj = Round.objects.filter(project_id=data.id, key=data.round_key).first()
influence_area_qs = InfluenceArea.objects.filter(round=round_obj)
if influence_area_qs.exists():
influence_area_obj = influence_area_qs.first()
influence_area_obj.influence_items.all().delete()
# 先删除再创建
data_list = []
for item in data.item_list:
new_item = InfluenceItem(influence=influence_area_obj,
change_type=item.change_type,
change_influ=item.change_influ,
change_des=item.change_des,
effect_cases=item.effect_cases)
data_list.append(new_item)
InfluenceItem.objects.bulk_create(data_list)
else:
parent_obj = InfluenceArea.objects.create(round=round_obj)
data_list = []
for item in data.item_list:
new_item = InfluenceItem(influence=parent_obj,
change_type=item.change_type,
change_influ=item.change_influ,
change_des=item.change_des,
effect_cases=item.effect_cases)
data_list.append(new_item)
InfluenceItem.objects.bulk_create(data_list)

View File

@@ -1,3 +1,5 @@
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
@@ -16,14 +18,16 @@ 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, \
TestDemandRelatedSchema, TestDemandExistRelatedSchema, DemandCopyToDesignSchema
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):
@@ -110,8 +114,9 @@ class TestDemandController(ControllerBase):
# ident判重
project_qs = Project.objects.filter(id=payload.project_id).first()
if payload.ident and project_qs:
exists = project_qs.ptField.filter(ident=payload.ident).exists()
if exists:
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
@@ -133,13 +138,13 @@ class TestDemandController(ControllerBase):
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']
subName=item['subName'],
subDescription=item['subDescription']
)
TestDemandContentStep.objects.bulk_create([
TestDemandContentStep(
@@ -150,6 +155,69 @@ class TestDemandController(ControllerBase):
])
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
@@ -161,9 +229,11 @@ class TestDemandController(ControllerBase):
for attr, value in payload.dict().items():
# 判重复
if attr == 'ident':
if testDemand_qs.ident != value: # 如果ident不和原来相等则要判重复
exists = project_qs.ptField.filter(ident=payload.ident).exists()
if exists:
# 先判断是否和原标识一样,且测试类型改变
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则不处理
@@ -181,7 +251,8 @@ class TestDemandController(ControllerBase):
if item['subName']:
content_obj = TestDemandContent.objects.create(
testDemand=testDemand_qs,
subName=item["subName"]
subName=item["subName"],
subDescription=item["subDescription"]
)
TestDemandContentStep.objects.bulk_create([
TestDemandContentStep(
@@ -221,7 +292,7 @@ class TestDemandController(ControllerBase):
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):
@@ -233,7 +304,7 @@ class TestDemandController(ControllerBase):
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}
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)
@@ -320,3 +391,11 @@ class TestDemandController(ControllerBase):
# 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)

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.3 on 2025-06-20 19:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0016_dutmetrics'),
]
operations = [
migrations.AlterField(
model_name='testdemandcontentstep',
name='id',
field=models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0 on 2025-12-15 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0017_alter_testdemandcontentstep_id'),
]
operations = [
migrations.AddField(
model_name='testdemandcontent',
name='subDescription',
field=models.CharField(blank=True, max_length=1024, null=True, verbose_name='测试子项一句话描述'),
),
]

View File

@@ -0,0 +1,45 @@
# Generated by Django 6.0.1 on 2026-02-02 15:00
import apps.project.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0018_testdemandcontent_subdescription'),
]
operations = [
migrations.CreateModel(
name='ProjectSoftSummary',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='projSoftSummary', serialize=False, to='project.project', verbose_name='关联项目')),
],
options={
'verbose_name': '软件概述表',
'verbose_name_plural': '软件概述表',
'db_table': 'project_soft_summary',
},
),
migrations.CreateModel(
name='StuctSortData',
fields=[
('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')),
('remark', models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述')),
('update_datetime', models.DateField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')),
('create_datetime', models.DateField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')),
('sort', models.IntegerField(blank=True, default=1, help_text='显示排序', null=True, verbose_name='显示排序')),
('type', models.CharField(choices=[('text', '文本'), ('table', '表格'), ('image', '图片')], default='text', max_length=20, verbose_name='数据类型')),
('fontnote', models.CharField(blank=True, default='', help_text='数据的题注说明', max_length=256, verbose_name='题注')),
('content', models.JSONField(default=apps.project.models.default_json_value, help_text='存储文本内容或二维表格数据或图片数据', verbose_name='内容')),
('soft_summary', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='data_schemas', to='project.projectsoftsummary', verbose_name='所属软件概述')),
],
options={
'verbose_name': '结构排序化数据',
'verbose_name_plural': '结构排序化数据',
'db_table': 'data_schemas',
},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.2 on 2026-02-04 10:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0019_projectsoftsummary_stuctsortdata'),
]
operations = [
migrations.AddField(
model_name='stuctsortdata',
name='project',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='data_schemas', to='project.project', verbose_name='该接口图所属的项目'),
),
migrations.AlterField(
model_name='stuctsortdata',
name='soft_summary',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='data_schemas', to='project.projectsoftsummary', verbose_name='所属软件概述'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 6.0.2 on 2026-02-04 10:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0020_stuctsortdata_project_and_more'),
]
operations = [
migrations.AlterField(
model_name='stuctsortdata',
name='project',
field=models.OneToOneField(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='data_schemas', to='project.project', verbose_name='该接口图所属的项目'),
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 6.0.2 on 2026-02-05 09:45
import apps.project.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0021_alter_stuctsortdata_project'),
]
operations = [
migrations.CreateModel(
name='DynamicHardwareTable',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='dynamic_hardware', serialize=False, to='project.project', verbose_name='关联项目')),
('table', models.JSONField(default=apps.project.models.default_json_value, help_text='储存表格二维数组', verbose_name='储存表格二维数组')),
],
options={
'verbose_name': '动态硬件项表',
'verbose_name_plural': '动态硬件项表',
'db_table': 'project_dynamic_hardware',
},
),
migrations.CreateModel(
name='DynamicSoftTable',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='dynamic_soft_item', serialize=False, to='project.project', verbose_name='关联项目')),
('table', models.JSONField(default=apps.project.models.default_json_value, help_text='储存表格二维数组', verbose_name='储存表格二维数组')),
],
options={
'verbose_name': '动态软件项表',
'verbose_name_plural': '动态软件项表',
'db_table': 'project_dynamic_soft_item',
},
),
migrations.CreateModel(
name='StaticSoftHardware',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='static_hardware', serialize=False, to='project.project', verbose_name='关联项目')),
('table', models.JSONField(default=apps.project.models.default_json_value, help_text='储存表格二维数组', verbose_name='储存表格二维数组')),
],
options={
'verbose_name': '静态硬件项表',
'verbose_name_plural': '静态硬件项表',
'db_table': 'project_static_hardware',
},
),
migrations.CreateModel(
name='StaticSoftItem',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='static_soft_item', serialize=False, to='project.project', verbose_name='关联项目')),
('table', models.JSONField(default=apps.project.models.default_json_value, help_text='储存表格二维数组', verbose_name='储存表格二维数组')),
],
options={
'verbose_name': '静态软件项表',
'verbose_name_plural': '静态软件项表',
'db_table': 'project_static_soft_item',
},
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0.2 on 2026-02-05 11:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0022_dynamichardwaretable_dynamicsofttable_and_more'),
]
operations = [
migrations.AddField(
model_name='dynamichardwaretable',
name='fontnote',
field=models.CharField(default='', help_text='数据的题注说明', max_length=256, null=True, verbose_name='题注'),
),
migrations.AddField(
model_name='dynamicsofttable',
name='fontnote',
field=models.CharField(default='', help_text='数据的题注说明', max_length=256, null=True, verbose_name='题注'),
),
migrations.AddField(
model_name='staticsofthardware',
name='fontnote',
field=models.CharField(default='', help_text='数据的题注说明', max_length=256, null=True, verbose_name='题注'),
),
migrations.AddField(
model_name='staticsoftitem',
name='fontnote',
field=models.CharField(default='', help_text='数据的题注说明', max_length=256, null=True, verbose_name='题注'),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 6.0.2 on 2026-02-05 17:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0023_dynamichardwaretable_fontnote_and_more'),
]
operations = [
migrations.CreateModel(
name='ProjectDynamicDescription',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='dynamic_des', serialize=False, to='project.project', verbose_name='关联项目')),
],
options={
'verbose_name': '动态环境描述',
'verbose_name_plural': '动态环境描述',
'db_table': 'project_dynamic_description',
},
),
migrations.AddField(
model_name='stuctsortdata',
name='dynamic_description',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='data_schemas', to='project.projectdynamicdescription', verbose_name='所属动态环境描述'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0.2 on 2026-02-06 10:56
import apps.project.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0024_projectdynamicdescription_and_more'),
]
operations = [
migrations.CreateModel(
name='EvaluateData',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='evaluate_data', serialize=False, to='project.project', verbose_name='关联项目')),
('table', models.JSONField(default=apps.project.models.default_json_value, help_text='储存表格二维数组', verbose_name='储存表格二维数组')),
('fontnote', models.CharField(default='', help_text='数据的题注说明', max_length=256, null=True, verbose_name='题注')),
],
options={
'verbose_name': '测评数据',
'verbose_name_plural': '测评数据',
'db_table': 'project_evaluate_data',
},
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 6.0.2 on 2026-02-06 13:56
import apps.project.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0025_evaluatedata'),
]
operations = [
migrations.CreateModel(
name='EnvAnalysis',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='env_analysis', serialize=False, to='project.project', verbose_name='关联项目')),
('table', models.JSONField(default=apps.project.models.default_json_value, help_text='储存表格二维数组', verbose_name='储存表格二维数组')),
('fontnote', models.CharField(default='', help_text='数据的题注说明', max_length=256, null=True, verbose_name='题注')),
('description', models.CharField(default='', max_length=1024, null=True, verbose_name='差异性分析文字')),
],
options={
'verbose_name': '环境差异性分析表',
'verbose_name_plural': '环境差异性分析表',
'db_table': 'project_env_analysis',
},
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 6.0.2 on 2026-02-06 16:05
import apps.project.models
import django.db.models.deletion
import tinymce.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0026_envanalysis'),
]
operations = [
migrations.CreateModel(
name='InfluenceArea',
fields=[
('project', models.OneToOneField(db_constraint=False, help_text='关联项目', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='influence', serialize=False, to='project.round', verbose_name='关联项目')),
],
options={
'verbose_name': '影响域分析',
'verbose_name_plural': '影响域分析',
'db_table': 'round_influence_area',
},
),
migrations.CreateModel(
name='InfluenceItem',
fields=[
('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')),
('remark', models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述')),
('update_datetime', models.DateField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')),
('create_datetime', models.DateField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')),
('sort', models.IntegerField(blank=True, default=1, help_text='显示排序', null=True, verbose_name='显示排序')),
('change_type', models.CharField(default='', help_text='更改类型', max_length=256, null=True, verbose_name='更改类型')),
('change_des', tinymce.models.HTMLField(blank=True, null=True, verbose_name='更改内容描述')),
('effect_cases', models.JSONField(default=apps.project.models.create_list, verbose_name='影响的用例key数组')),
('influence', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='influence_items', to='project.influencearea', verbose_name='所属影响域分析')),
],
options={
'verbose_name': '影响域分析 - 行数据',
'verbose_name_plural': '影响域分析 - 行数据',
'db_table': 'influence_item',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-07 13:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('project', '0027_influencearea_influenceitem'),
]
operations = [
migrations.RenameField(
model_name='influencearea',
old_name='project',
new_name='round',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-07 16:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0028_rename_project_influencearea_round'),
]
operations = [
migrations.AddField(
model_name='influenceitem',
name='change_influ',
field=models.TextField(default='', help_text='影响域分析', max_length=2048, null=True, verbose_name='影响域分析'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.4 on 2026-04-17 13:57
import apps.project.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0029_influenceitem_change_influ'),
]
operations = [
migrations.AlterField(
model_name='project',
name='devplant',
field=models.JSONField(blank=True, default=apps.project.models.create_list, help_text='开发环境', null=True, verbose_name='开发环境'),
),
migrations.AlterField(
model_name='project',
name='runtime',
field=models.JSONField(blank=True, default=apps.project.models.create_list, help_text='运行环境', null=True, verbose_name='运行环境'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-17 16:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0030_alter_project_devplant_alter_project_runtime'),
]
operations = [
migrations.AlterField(
model_name='testdemand',
name='adequacy',
field=models.CharField(blank=True, help_text='充分条件', max_length=2048, null=True, verbose_name='充分条件'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-20 10:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0031_alter_testdemand_adequacy'),
]
operations = [
migrations.AlterField(
model_name='design',
name='protocal',
field=models.CharField(blank=True, default='', help_text='接口数据', max_length=1024, null=True, verbose_name='接口数据'),
),
]

View File

@@ -0,0 +1,51 @@
# Generated by Django 6.0.4 on 2026-04-21 18:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0032_alter_design_protocal'),
]
operations = [
migrations.RemoveField(
model_name='design',
name='protocal',
),
migrations.RemoveField(
model_name='design',
name='to',
),
migrations.RemoveField(
model_name='design',
name='type',
),
migrations.AlterField(
model_name='design',
name='source',
field=models.CharField(blank=True, default='', help_text='接口来源', max_length=256, null=True, verbose_name='接口来源'),
),
migrations.CreateModel(
name='JKDesignInfo',
fields=[
('id', models.BigAutoField(help_text='Id', primary_key=True, serialize=False, verbose_name='Id')),
('remark', models.CharField(blank=True, help_text='描述', max_length=255, null=True, verbose_name='描述')),
('update_datetime', models.DateField(auto_now=True, help_text='修改时间', null=True, verbose_name='修改时间')),
('create_datetime', models.DateField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')),
('sort', models.IntegerField(blank=True, default=1, help_text='显示排序', null=True, verbose_name='显示排序')),
('direction', models.CharField(choices=[('forward', '正向'), ('reverse', '反向')], max_length=10, verbose_name='方向')),
('description', models.TextField(blank=True, default='', max_length=1024, null=True, verbose_name='接口描述')),
('source', models.CharField(blank=True, default='', max_length=200, null=True, verbose_name='来源')),
('destination', models.CharField(blank=True, default='', max_length=200, null=True, verbose_name='目的地')),
('jk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jkField', to='project.design', verbose_name='所属接口Design')),
],
options={
'verbose_name': '接口一个方向的信息',
'verbose_name_plural': '接口一个方向的信息',
'unique_together': {('jk', 'direction')},
},
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 6.0.4 on 2026-04-21 18:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0033_remove_design_protocal_remove_design_to_and_more'),
]
operations = [
migrations.RemoveField(
model_name='design',
name='source',
),
migrations.AddField(
model_name='design',
name='type',
field=models.CharField(blank=True, default='', help_text='接口类型', max_length=1024, null=True, verbose_name='接口类型'),
),
migrations.AlterField(
model_name='jkdesigninfo',
name='description',
field=models.TextField(blank=True, max_length=1024, null=True, verbose_name='接口描述'),
),
migrations.AlterField(
model_name='jkdesigninfo',
name='destination',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='目的地'),
),
migrations.AlterField(
model_name='jkdesigninfo',
name='source',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='来源'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-21 18:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('project', '0034_remove_design_source_design_type_and_more'),
]
operations = [
migrations.AddField(
model_name='design',
name='is_bidirectional',
field=models.BooleanField(default=False, verbose_name='是否双向'),
),
]

View File

@@ -13,58 +13,43 @@ class Project(CoreModel):
objects = models.Manager()
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="项目标识",
help_text="项目标识", unique=True) # 唯一
name = models.CharField(max_length=100, blank=True, null=True, verbose_name="项目名称", help_text="项目名称")
beginTime = models.DateField(auto_now_add=True, null=True, blank=True, help_text="开始时间",
name = models.CharField(max_length=100, blank=True, null=True, verbose_name="项目名称",
help_text="项目名称")
beginTime = models.DateField(null=True, blank=True, help_text="开始时间",
verbose_name="开始时间")
endTime = models.DateField(auto_now_add=True, null=True, blank=True, help_text="结束时间", verbose_name="结束时间")
endTime = models.DateField(null=True, blank=True, help_text="结束时间",
verbose_name="结束时间")
duty_person = models.CharField(max_length=64, verbose_name="负责人", help_text="负责人")
member = models.JSONField(null=True, blank=True, help_text="项目成员", verbose_name="项目成员", default=create_list)
member = models.JSONField(null=True, blank=True, help_text="项目成员", verbose_name="项目成员",
default=create_list)
# 8月新增字段
quality_person = models.CharField(max_length=64, verbose_name="质量保证员", help_text="质量保证员")
vise_person = models.CharField(max_length=64, verbose_name="质量监督员", help_text="质量监督员")
config_person = models.CharField(max_length=64, verbose_name="配置管理员", help_text="配置管理员")
# ~~~~~~~~~~~
security_level = models.CharField(max_length=8, blank=True, null=True, verbose_name="安全等级",
help_text="安全等级")
test_level = models.JSONField(null=True, blank=True, help_text="测试级别", verbose_name="测试级别",
default=create_list)
plant_type = models.JSONField(null=True, blank=True, help_text="平台类型", verbose_name="平台类型",
default=create_list)
security_level = models.CharField(max_length=8, blank=True, null=True, verbose_name="安全等级", help_text="安全等级")
test_level = models.JSONField(null=True, blank=True, help_text="测试级别", verbose_name="测试级别", default=create_list)
plant_type = models.JSONField(null=True, blank=True, help_text="平台类型", verbose_name="平台类型", default=create_list)
report_type = models.CharField(max_length=64, blank=True, null=True, verbose_name="报告类型", help_text="报告类型")
language = models.JSONField(null=True, blank=True, help_text="被测语言", verbose_name="被测语言",
default=create_list)
standard = models.JSONField(null=True, blank=True, help_text="依据标准", verbose_name="依据标准",
default=create_list)
language = models.JSONField(null=True, blank=True, help_text="被测语言", verbose_name="被测语言", default=create_list)
standard = models.JSONField(null=True, blank=True, help_text="依据标准", verbose_name="依据标准", default=create_list)
entrust_unit = models.CharField(max_length=64, verbose_name="委托方单位", help_text="委托方单位")
entrust_contact = models.CharField(max_length=64, blank=True, null=True, verbose_name="委托方联系人",
help_text="委托方联系人")
entrust_contact_phone = models.CharField(max_length=64, blank=True, null=True, verbose_name="委托方电话",
help_text="委托方电话")
entrust_email = models.CharField(max_length=64, blank=True, null=True, verbose_name="委托方邮箱",
help_text="委托方邮箱")
entrust_contact = models.CharField(max_length=64, blank=True, null=True, verbose_name="委托方联系人", help_text="委托方联系人")
entrust_contact_phone = models.CharField(max_length=64, blank=True, null=True, verbose_name="委托方电话", help_text="委托方电话")
entrust_email = models.CharField(max_length=64, blank=True, null=True, verbose_name="委托方邮箱", help_text="委托方邮箱")
dev_unit = models.CharField(max_length=64, verbose_name="开发方单位", help_text="开发方单位")
dev_contact = models.CharField(max_length=64, blank=True, null=True, verbose_name="研制方联系人",
help_text="研制方联系人")
dev_contact_phone = models.CharField(max_length=64, blank=True, null=True, verbose_name="研制方电话",
help_text="研制方电话")
dev_email = models.CharField(max_length=64, blank=True, null=True, verbose_name="研制方邮箱",
help_text="研制方邮箱")
dev_contact = models.CharField(max_length=64, blank=True, null=True, verbose_name="研制方联系人", help_text="研制方联系人")
dev_contact_phone = models.CharField(max_length=64, blank=True, null=True, verbose_name="研制方电话", help_text="研制方电话")
dev_email = models.CharField(max_length=64, blank=True, null=True, verbose_name="研制方邮箱", help_text="研制方邮箱")
test_unit = models.CharField(max_length=64, verbose_name="测试方单位", help_text="测试方单位")
test_contact = models.CharField(max_length=64, blank=True, null=True, verbose_name="测评中心联系人",
help_text="测评中心联系人")
test_contact_phone = models.CharField(max_length=64, blank=True, null=True, verbose_name="测评中心电话",
help_text="测评中心电话")
test_email = models.CharField(max_length=64, blank=True, null=True, verbose_name="测评中心邮箱",
help_text="测评中心邮箱")
test_contact = models.CharField(max_length=64, blank=True, null=True, verbose_name="测评中心联系人", help_text="测评中心联系人")
test_contact_phone = models.CharField(max_length=64, blank=True, null=True, verbose_name="测评中心电话", help_text="测评中心电话")
test_email = models.CharField(max_length=64, blank=True, null=True, verbose_name="测评中心邮箱", help_text="测评中心邮箱")
step = models.CharField(max_length=8, blank=True, null=True, verbose_name="项目阶段", help_text="项目阶段")
abbreviation = models.JSONField(null=True, blank=True, help_text="缩略语", verbose_name="缩略语",
default=create_list)
soft_type = models.SmallIntegerField(verbose_name='软件类型', choices=((1, '新研'), (2, '改造'), (3, '沿用')),
default=1)
runtime = models.CharField(max_length=8, blank=True, null=True, verbose_name="运行环境",
help_text="运行环境")
devplant = models.CharField(max_length=8, blank=True, null=True, verbose_name="开发环境",
help_text="开发环境")
abbreviation = models.JSONField(null=True, blank=True, help_text="缩略语", verbose_name="缩略语", default=create_list)
soft_type = models.SmallIntegerField(verbose_name='软件类型', choices=((1, '新研'), (2, '改造'), (3, '沿用')), default=1)
runtime = models.JSONField(null=True, blank=True, help_text="运行环境", verbose_name="运行环境", default=create_list)
devplant = models.JSONField(null=True, blank=True, help_text="开发环境", verbose_name="开发环境", default=create_list)
# 9月2日新增字段密级
secret = models.CharField(max_length=30, default='1', verbose_name='密级', help_text='密级')
@@ -85,23 +70,31 @@ class Round(CoreModel):
help_text="轮次名称")
beginTime = models.DateField(auto_now_add=True, null=True, blank=True, help_text="开始时间",
verbose_name="开始时间")
endTime = models.DateField(auto_now_add=True, null=True, blank=True, help_text="结束时间", verbose_name="结束时间")
grade = models.CharField(max_length=64, blank=True, null=True, verbose_name="等级", help_text="等级", default='1')
best_condition_voltage = models.CharField(max_length=64, blank=True, null=True, verbose_name="最优工况电压",
endTime = models.DateField(auto_now_add=True, null=True, blank=True, help_text="结束时间",
verbose_name="结束时间")
grade = models.CharField(max_length=64, blank=True, null=True, verbose_name="等级", help_text="等级",
default='1')
best_condition_voltage = models.CharField(max_length=64, blank=True, null=True,
verbose_name="最优工况电压",
help_text="最优工况电压")
best_condition_tem = models.CharField(max_length=64, blank=True, null=True, verbose_name="最优工况温度",
help_text="最优工况温度")
typical_condition_voltage = models.CharField(max_length=64, blank=True, null=True, verbose_name="典型工况电压",
typical_condition_voltage = models.CharField(max_length=64, blank=True, null=True,
verbose_name="典型工况电压",
help_text="典型工况电压")
typical_condition_tem = models.CharField(max_length=64, blank=True, null=True, verbose_name="典型工况温度",
typical_condition_tem = models.CharField(max_length=64, blank=True, null=True,
verbose_name="典型工况温度",
help_text="典型工况温度")
low_condition_voltage = models.CharField(max_length=64, blank=True, null=True, verbose_name="最低工况电压",
low_condition_voltage = models.CharField(max_length=64, blank=True, null=True,
verbose_name="最低工况电压",
help_text="最低工况电压")
low_condition_tem = models.CharField(max_length=64, blank=True, null=True, verbose_name="最低工况温度",
help_text="最低工况温度")
project = models.ForeignKey(to="Project", db_constraint=False, related_name="pField", on_delete=models.CASCADE,
project = models.ForeignKey(to="Project", db_constraint=False, related_name="pField",
on_delete=models.CASCADE,
verbose_name='归属项目', help_text='归属项目', related_query_name='pQuery')
level = models.CharField(max_length=15, verbose_name='树状级别第一级', help_text="树状级别第一级", default='0')
level = models.CharField(max_length=15, verbose_name='树状级别第一级', help_text="树状级别第一级",
default='0')
key = models.CharField(max_length=15, verbose_name='给前端的树状级别', help_text="给前端的树状级别")
title = models.CharField(max_length=15, verbose_name='给前端的name', help_text="给前端的name")
# 新增执行地点
@@ -120,30 +113,38 @@ class Dut(CoreModel):
objects = models.Manager()
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="被测件标识",
help_text="被测件标识") # 后面加上unique=True
type = models.CharField(max_length=16, blank=True, null=True, verbose_name="被测件类型", help_text="被测件类型")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="被测件名称", help_text="被测件名称")
type = models.CharField(max_length=16, blank=True, null=True, verbose_name="被测件类型",
help_text="被测件类型")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="被测件名称",
help_text="被测件名称")
# 2025年4月28日更新分为总函数、有效代码行数、注释行数
total_lines = models.CharField(max_length=64, blank=True, null=True, verbose_name='总行数')
effective_lines = models.CharField(max_length=64, blank=True, null=True, verbose_name='有效代码行数')
comment_lines = models.CharField(max_length=64, blank=True, null=True, verbose_name='注释行数')
# 更新结束
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称", help_text="树-名称")
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称",
help_text="树-名称")
key = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-key", help_text="树-key")
# 被测件添加版本、发布单位、发布时间
version = models.CharField(max_length=64, blank=True, null=True, verbose_name="发布版本", help_text="发布版本")
version = models.CharField(max_length=64, blank=True, null=True, verbose_name="发布版本",
help_text="发布版本")
release_union = models.CharField(max_length=64, blank=True, null=True, verbose_name="发布版本",
help_text="发布版本")
release_date = models.DateField(auto_now_add=True, null=True, blank=True, help_text="发布时间",
verbose_name="发布时间")
# 新增用户文档的编号
ref = models.CharField(max_length=32, blank=True, null=True, verbose_name="文档编号", help_text="文档编号")
ref = models.CharField(max_length=32, blank=True, null=True, verbose_name="文档编号",
help_text="文档编号")
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level", help_text="树-level",
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level",
help_text="树-level",
default=1) # 默认为1
project = models.ForeignKey(to="Project", db_constraint=False, related_name="pdField", on_delete=models.CASCADE,
project = models.ForeignKey(to="Project", db_constraint=False, related_name="pdField",
on_delete=models.CASCADE,
verbose_name='归属项目', help_text='归属项目', related_query_name='pdQuery')
round = models.ForeignKey(to="Round", db_constraint=False, related_name="rdField", on_delete=models.CASCADE,
round = models.ForeignKey(to="Round", db_constraint=False, related_name="rdField",
on_delete=models.CASCADE,
verbose_name='归属轮次', help_text='归属轮次', related_query_name='rdQuery')
def __str__(self):
@@ -159,7 +160,8 @@ class DutMetrics(models.Model):
objects = models.Manager()
id = ShortUUIDField(primary_key=True, help_text="id", verbose_name="id")
# 外键Dut一个Dut储存一个指标
dut = models.OneToOneField(Dut, on_delete=models.CASCADE, related_name='metrics', related_query_name='metrics',
dut = models.OneToOneField(Dut, on_delete=models.CASCADE, related_name='metrics',
related_query_name='metrics',
db_constraint=False, verbose_name='归属源代码被测件')
avg_function_lines = models.IntegerField(verbose_name='平均模块大小')
avg_cyclomatic = models.IntegerField(verbose_name='平均圈复杂度')
@@ -173,32 +175,35 @@ class Design(CoreModel):
objects = models.Manager()
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="设计需求标识",
help_text="设计需求标识")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="设计需求名称", help_text="设计需求名称")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="设计需求名称",
help_text="设计需求名称")
demandType = models.CharField(max_length=8, blank=True, null=True, verbose_name="设计需求类型",
help_text="设计需求类型")
description = HTMLField(blank=True, null=True, verbose_name="设计需求描述", help_text="设计需求描述")
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称", help_text="树-名称")
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称",
help_text="树-名称")
key = models.CharField(max_length=64, blank=True, null=True, verbose_name="round-dut-designkey",
help_text="round-dut-designkey")
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level", help_text="树-level",
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level",
help_text="树-level",
default=2) # 默认为2
chapter = models.CharField(max_length=64, blank=True, verbose_name="设计需求章节号", help_text="设计需求章节号")
project = models.ForeignKey(to="Project", db_constraint=False, related_name="psField", on_delete=models.CASCADE,
chapter = models.CharField(max_length=64, blank=True, verbose_name="设计需求章节号",
help_text="设计需求章节号")
project = models.ForeignKey(to="Project", db_constraint=False, related_name="psField",
on_delete=models.CASCADE,
verbose_name='归属项目', help_text='归属项目', related_query_name='psQuery')
round = models.ForeignKey(to="Round", db_constraint=False, related_name="dsField", on_delete=models.CASCADE,
round = models.ForeignKey(to="Round", db_constraint=False, related_name="dsField",
on_delete=models.CASCADE,
verbose_name='归属轮次', help_text='归属轮次', related_query_name='rsQuery')
dut = models.ForeignKey(to="Dut", db_constraint=False, related_name="rsField", on_delete=models.CASCADE,
verbose_name='归属轮次', help_text='归属轮次', related_query_name='rsQuery')
# 如果是demandTye='3'则加上如下字段
source = models.CharField(max_length=64, blank=True, null=True, default='', verbose_name='接口来源',
help_text='接口来源')
to = models.CharField(max_length=64, blank=True, null=True, default='', verbose_name='接口目的地',
help_text='接口目的地')
type = models.CharField(max_length=64, blank=True, null=True, default='', verbose_name='接口类型',
type = models.CharField(max_length=1024, blank=True, null=True, default='', verbose_name='接口类型',
help_text='接口类型')
# 注意:该字段改为接口数据
protocal = models.CharField(max_length=64, blank=True, null=True, default='', verbose_name='接口数据',
help_text='接口数据')
is_bidirectional = models.BooleanField(
default=False,
verbose_name="是否双向"
)
def __str__(self):
return f'设计需求:{self.name}'
@@ -209,34 +214,62 @@ class Design(CoreModel):
verbose_name_plural = verbose_name
ordering = ('key',)
class JKDesignInfo(CoreModel):
class Direction(models.TextChoices):
FORWARD = 'forward', '正向'
REVERSE = 'reverse', '反向'
jk = models.ForeignKey(Design, on_delete=models.CASCADE, related_name="jkField", verbose_name="所属接口Design")
direction = models.CharField(
max_length=10,
choices=Direction.choices, # type: ignore
verbose_name="方向"
)
description = models.TextField(max_length=1024, blank=True, null=True, verbose_name="接口描述")
source = models.CharField(max_length=200, blank=True, null=True, verbose_name="来源")
destination = models.CharField(max_length=200, blank=True, null=True, verbose_name="目的地")
class Meta:
unique_together = [['jk', 'direction']] # 同一个方向仅一条记录
verbose_name = "接口一个方向的信息"
verbose_name_plural = "接口一个方向的信息"
def __str__(self):
return f"{self.jk.name} - {self.get_direction_display()}"
class TestDemand(CoreModel):
objects = models.Manager()
"""测试项"""
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="测试需求标识",
help_text="测试需求标识")
objects = models.Manager()
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="测试需求标识", help_text="测试需求标识")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="测试需求名称", help_text="测试需求名称")
adequacy = models.CharField(max_length=256, blank=True, null=True, verbose_name="充分条件", help_text="充分条件")
adequacy = models.CharField(max_length=2048, blank=True, null=True, verbose_name="充分条件", help_text="充分条件")
priority = models.CharField(max_length=8, blank=True, null=True, verbose_name="优先级", help_text="优先级")
testType = models.CharField(max_length=8, null=True, blank=True, help_text="测试类型", verbose_name="测试类型",
default="1")
testType = models.CharField(max_length=8, null=True, blank=True, help_text="测试类型", verbose_name="测试类型", default="1")
testMethod = models.JSONField(blank=True, help_text="测试方法", verbose_name="测试方法", default=create_list)
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称", help_text="树-名称")
key = models.CharField(max_length=64, blank=True, null=True, verbose_name="round-dut-designkey-testdemand",
key = models.CharField(max_length=64, blank=True, null=True,
verbose_name="round-dut-designkey-testdemand",
help_text="round-dut-designkey-testdemand")
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level", help_text="树-level",
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level",
help_text="树-level",
default=3) # 默认为3
project = models.ForeignKey(to="Project", db_constraint=False, related_name="ptField", on_delete=models.CASCADE,
project = models.ForeignKey(to="Project", db_constraint=False, related_name="ptField",
on_delete=models.CASCADE,
verbose_name='归属项目', help_text='归属项目', related_query_name='ptQuery')
round = models.ForeignKey(to="Round", db_constraint=False, related_name="rtField", on_delete=models.CASCADE,
round = models.ForeignKey(to="Round", db_constraint=False, related_name="rtField",
on_delete=models.CASCADE,
verbose_name='归属轮次', help_text='归属轮次', related_query_name='dutQuery')
dut = models.ForeignKey(to="Dut", db_constraint=False, related_name="dutField", on_delete=models.CASCADE,
verbose_name='归属被测件', help_text='归属被测件', related_query_name='dtQuery')
design = models.ForeignKey(to="Design", db_constraint=False, related_name="dtField", on_delete=models.CASCADE,
verbose_name='归属设计需求', help_text='归属设计需求', related_query_name='dtQuery')
design = models.ForeignKey(to="Design", db_constraint=False, related_name="dtField",
on_delete=models.CASCADE,
verbose_name='归属设计需求', help_text='归属设计需求',
related_query_name='dtQuery')
otherDesign = models.ManyToManyField(to="Design", db_constraint=False, related_name="odField",
related_query_name='odQuery', blank=True)
# 新模版要求:测试项描述对整个测试项进行描述
testDesciption = models.CharField(max_length=1024, blank=True, null=True, verbose_name='测试项描述', default="",
testDesciption = models.CharField(max_length=1024, blank=True, null=True, verbose_name='测试项描述',
default="",
help_text='测试项描述')
def __str__(self):
@@ -246,11 +279,15 @@ class TestDemandContent(CoreModel):
objects = models.Manager()
"""测试方法中的测试子项内容"""
testDemand = models.ForeignKey(to="TestDemand", db_constraint=False, related_name="testQField",
on_delete=models.CASCADE, verbose_name='归属的测试项', help_text='归属的测试项',
on_delete=models.CASCADE, verbose_name='归属的测试项',
help_text='归属的测试项',
related_query_name='testQField')
# 2025年4月16去掉subDesc、condition、observe
# 4月17修改因为新增步骤所以把operation和expect弄到下面Model里面了新增字段
# 2025/4/16去掉subDesc、condition、observe
# 2025/4/17修改因为新增步骤所以把operation和expect弄到下面Model里面了新增字段
subName = models.CharField(max_length=1024, blank=True, null=True, verbose_name='测试子项名称')
# 2025/12/15修改CPU新增“测试子项描述”FPGA渲染单个
subDescription = models.CharField(max_length=1024, blank=True, null=True,
verbose_name='测试子项一句话描述')
def __str__(self):
return f'测试子项:{self.subName}'
@@ -258,41 +295,57 @@ class TestDemandContent(CoreModel):
# 4月17日新增因为测试项需要测试子项step
class TestDemandContentStep(CoreModel):
objects = models.Manager()
id = ShortUUIDField(primary_key=True, help_text="Id", verbose_name="Id")
operation = models.CharField(max_length=3072, blank=True, null=True, verbose_name='测试子项操作')
expect = models.CharField(max_length=1024, blank=True, null=True, verbose_name='期望')
testDemandContent = models.ForeignKey(to="TestDemandContent", db_constraint=False, related_name="testStepField",
testDemandContent = models.ForeignKey(to="TestDemandContent", db_constraint=False,
related_name="testStepField",
on_delete=models.CASCADE, verbose_name='归属的测试项',
help_text='归属的测试项',
related_query_name='testStepField')
class Case(CoreModel):
objects = models.Manager()
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="用例标识", help_text="用例标识")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="用例名称", help_text="用例名称")
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="用例标识",
help_text="用例标识")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="用例名称",
help_text="用例名称")
initialization = models.CharField(max_length=128, blank=True, null=True, verbose_name="初始条件",
help_text="初始化条件")
premise = models.CharField(max_length=128, blank=True, null=True, verbose_name="前提和约束", help_text="前提和约束")
summarize = models.CharField(max_length=256, blank=True, null=True, verbose_name="用例综述", help_text="用例综述")
designPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="设计人员", help_text="设计人员")
testPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="测试人员", help_text="测试人员")
premise = models.CharField(max_length=128, blank=True, null=True, verbose_name="前提和约束",
help_text="前提和约束")
summarize = models.CharField(max_length=256, blank=True, null=True, verbose_name="用例综述",
help_text="用例综述")
designPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="设计人员",
help_text="设计人员")
testPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="测试人员",
help_text="测试人员")
monitorPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="审核人员",
help_text="审核人员")
project = models.ForeignKey(to="Project", db_constraint=False, related_name="pcField", on_delete=models.CASCADE,
project = models.ForeignKey(to="Project", db_constraint=False, related_name="pcField",
on_delete=models.CASCADE,
verbose_name='归属项目', help_text='归属项目', related_query_name='pcQuery')
isLeaf = models.BooleanField(default=True, verbose_name="树状图最后一个节点", help_text="树状图最后一个节点")
round = models.ForeignKey(to="Round", db_constraint=False, related_name="rcField", on_delete=models.CASCADE,
isLeaf = models.BooleanField(default=True, verbose_name="树状图最后一个节点",
help_text="树状图最后一个节点")
round = models.ForeignKey(to="Round", db_constraint=False, related_name="rcField",
on_delete=models.CASCADE,
verbose_name='归属轮次', help_text='归属轮次', related_query_name='rcQuery')
dut = models.ForeignKey(to="Dut", db_constraint=False, related_name="ducField", on_delete=models.CASCADE,
verbose_name='归属被测件', help_text='归属被测件', related_query_name='ducQuery')
design = models.ForeignKey(to="Design", db_constraint=False, related_name="dcField", on_delete=models.CASCADE,
verbose_name='归属设计需求', help_text='归属设计需求', related_query_name='dcQuery')
test = models.ForeignKey(to="TestDemand", db_constraint=False, related_name="tcField", on_delete=models.CASCADE,
verbose_name='归属测试需求', help_text='归属测试需求', related_query_name='tcQuery')
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称", help_text="树-名称")
key = models.CharField(max_length=64, blank=True, null=True, verbose_name="round-dut-designkey-testdemand-case",
design = models.ForeignKey(to="Design", db_constraint=False, related_name="dcField",
on_delete=models.CASCADE,
verbose_name='归属设计需求', help_text='归属设计需求',
related_query_name='dcQuery')
test = models.ForeignKey(to="TestDemand", db_constraint=False, related_name="tcField",
on_delete=models.CASCADE,
verbose_name='归属测试需求', help_text='归属测试需求',
related_query_name='tcQuery')
title = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-名称",
help_text="树-名称")
key = models.CharField(max_length=64, blank=True, null=True,
verbose_name="round-dut-designkey-testdemand-case",
help_text="round-dut-designkey-testdemand-case")
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level", help_text="树-level",
level = models.CharField(max_length=64, blank=True, null=True, verbose_name="树-level",
help_text="树-level",
default=4) # 默认为4
# 2024年5月31日新增属性执行时间
exe_time = models.DateField(blank=True, null=True, verbose_name='执行时间', help_text='执行时间')
@@ -311,14 +364,17 @@ class Case(CoreModel):
class CaseStep(CoreModel):
objects = models.Manager()
operation = HTMLField(blank=True, null=True, verbose_name="测试步骤-操作", help_text="测试步骤-操作")
expect = models.CharField(max_length=3072, blank=True, null=True, verbose_name="用例预期", help_text="用例预期")
expect = models.CharField(max_length=3072, blank=True, null=True, verbose_name="用例预期",
help_text="用例预期")
result = HTMLField(blank=True, null=True, verbose_name="测试步骤-结果", help_text="测试步骤-结果")
passed = models.CharField(max_length=8, null=True, blank=True, help_text="是否通过", verbose_name="是否通过",
passed = models.CharField(max_length=8, null=True, blank=True, help_text="是否通过",
verbose_name="是否通过",
default="3")
# status = models.CharField(max_length=8, null=True, blank=True, help_text="执行状态", verbose_name="执行状态",
# default="3")
case = models.ForeignKey(to="Case", db_constraint=False, related_name="step",
on_delete=models.CASCADE, verbose_name='归属的测试用例', help_text='归属的测试用例',
on_delete=models.CASCADE, verbose_name='归属的测试用例',
help_text='归属的测试用例',
related_query_name='stepQ')
def __str__(self):
@@ -327,34 +383,46 @@ class CaseStep(CoreModel):
class Problem(CoreModel):
objects = models.Manager()
# ident为PT_RXXXX_ident这里需要根据测试项类型进行排序处理
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="问题单标识", help_text="问题单标识")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="问题单名称", help_text="问题单名称")
ident = models.CharField(max_length=64, blank=True, null=True, verbose_name="问题单标识",
help_text="问题单标识")
name = models.CharField(max_length=64, blank=True, null=True, verbose_name="问题单名称",
help_text="问题单名称")
# 问题状态1-已闭环 2-开放 3-推迟 4-撤销
status = models.CharField(max_length=8, blank=True, null=True, verbose_name="缺陷状态", help_text="缺陷状态")
status = models.CharField(max_length=8, blank=True, null=True, verbose_name="缺陷状态",
help_text="缺陷状态")
# 问题等级1-一般 2-严重 3-建议 4-重大
grade = models.CharField(max_length=8, blank=True, null=True, verbose_name="缺陷等级", help_text="缺陷等级")
grade = models.CharField(max_length=8, blank=True, null=True, verbose_name="缺陷等级",
help_text="缺陷等级")
# 问题类型1-其他问题 2-文档问题 3-程序问题 4-设计问题 5-需求问题 6-数据问题
type = models.CharField(max_length=8, blank=True, null=True, verbose_name="缺陷类型", help_text="缺陷类型")
type = models.CharField(max_length=8, blank=True, null=True, verbose_name="缺陷类型",
help_text="缺陷类型")
closeMethod = models.JSONField(null=True, blank=True, help_text="闭环方式", verbose_name="闭环方式",
default=create_list_1)
operation = HTMLField(blank=True, null=True, verbose_name="问题描述", help_text="问题描述")
result = HTMLField(blank=True, null=True, verbose_name="问题结果/影响", help_text="问题结果/影响")
postPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="提出人员", help_text="提出人员")
postDate = models.DateField(auto_now_add=True, null=True, blank=True, help_text="提单日期", verbose_name="提单日期")
postPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="提出人员",
help_text="提出人员")
postDate = models.DateField(auto_now_add=True, null=True, blank=True, help_text="提单日期",
verbose_name="提单日期")
designerPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="开发人员",
help_text="开发人员")
designDate = models.DateField(auto_now_add=True, null=True, blank=True, help_text="确认日期",
verbose_name="确认日期")
verifyPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="验证人员", help_text="验证人员")
verifyPerson = models.CharField(max_length=16, blank=True, null=True, verbose_name="验证人员",
help_text="验证人员")
verifyDate = models.DateField(auto_now_add=True, null=True, blank=True, help_text="验证日期",
verbose_name="验证日期")
project = models.ForeignKey(to="Project", db_constraint=False, related_name="projField", on_delete=models.CASCADE,
project = models.ForeignKey(to="Project", db_constraint=False, related_name="projField",
on_delete=models.CASCADE,
verbose_name='归属项目', help_text='归属项目', related_query_name='projQuery')
case = models.ManyToManyField(to="Case", db_constraint=False, related_name="caseField", verbose_name='归属测试用例',
case = models.ManyToManyField(to="Case", db_constraint=False, related_name="caseField",
verbose_name='归属测试用例',
help_text='归属测试用例-多对多', related_query_name='caseQuery')
solve = models.TextField(verbose_name='开发人员填写-改正措施',
help_text='开发人员填写-改正措施该字段需要关联“status=1”', blank=True, null=True)
analysis = HTMLField(blank=True, null=True, verbose_name="开发人员填写-原因分析", help_text="开发人员填写-原因分析")
help_text='开发人员填写-改正措施该字段需要关联“status=1”', blank=True,
null=True)
analysis = HTMLField(blank=True, null=True, verbose_name="开发人员填写-原因分析",
help_text="开发人员填写-原因分析")
effect_scope = HTMLField(blank=True, null=True, verbose_name="开发人员填写-影响域分析",
help_text="开发人员填写-影响域分析")
verify_result = HTMLField(blank=True, null=True, verbose_name="回归结果", help_text="回归结果")
@@ -389,6 +457,9 @@ class Contact(CoreModel):
ordering = ('create_datetime',)
# ~~~~~2024年2月27日新增~~~~~
def default_json_value():
return ""
class Abbreviation(models.Model):
objects = models.Manager()
title = models.CharField(max_length=64, verbose_name="缩略语", help_text="缩略语")
@@ -401,3 +472,163 @@ class Abbreviation(models.Model):
db_table = 'project_abbreviation'
verbose_name = '缩略语和行业词汇'
verbose_name_plural = '缩略语和行业词汇'
# 一对一项目model软件概述
class ProjectSoftSummary(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="projSoftSummary", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
class Meta:
db_table = 'project_soft_summary'
verbose_name = "软件概述表"
verbose_name_plural = verbose_name
# 一对一项目model动态测试环境描述
class ProjectDynamicDescription(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="dynamic_des", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
class Meta:
db_table = 'project_dynamic_description'
verbose_name = "动态环境描述"
verbose_name_plural = verbose_name
# 一对一项目model静态软件项表
class StaticSoftItem(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="static_soft_item", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_static_soft_item'
verbose_name = "静态软件项表"
verbose_name_plural = verbose_name
# 一对一项目model静态硬件项表
class StaticSoftHardware(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="static_hardware", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_static_hardware'
verbose_name = "静态硬件项表"
verbose_name_plural = verbose_name
# 一对一项目model动态软件项表
class DynamicSoftTable(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="dynamic_soft_item", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_dynamic_soft_item'
verbose_name = "动态软件项表"
verbose_name_plural = verbose_name
# 一对一项目model动态硬件项
class DynamicHardwareTable(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="dynamic_hardware", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_dynamic_hardware'
verbose_name = "动态硬件项表"
verbose_name_plural = verbose_name
# 一对一项目model动态环境 - 测评数据
class EvaluateData(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="evaluate_data", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_evaluate_data'
verbose_name = "测评数据"
verbose_name_plural = verbose_name
# 一对一项目model环境差异性分析
class EnvAnalysis(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False,
related_name="env_analysis", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
description = models.CharField(max_length=1024, null=True, default="", verbose_name="差异性分析文字")
class Meta:
db_table = 'project_env_analysis'
verbose_name = "环境差异性分析表"
verbose_name_plural = verbose_name
# 结构化排序数据
class StuctSortData(CoreModel):
"""
与其他项目信息的多对一关系
"""
# 软件概述内容
soft_summary = models.ForeignKey(ProjectSoftSummary, db_constraint=False, related_name="data_schemas", verbose_name="所属软件概述",
on_delete=models.CASCADE, null=True, blank=True)
# 接口图
project = models.OneToOneField(Project, db_constraint=False, related_name="data_schemas", on_delete=models.CASCADE, null=True, blank=True,
verbose_name="该接口图所属的项目")
# 动态环境描述
dynamic_description = models.ForeignKey(ProjectDynamicDescription, db_constraint=False, related_name="data_schemas",
verbose_name="所属动态环境描述",
on_delete=models.CASCADE, null=True, blank=True)
type = models.CharField(
max_length=20,
choices=(('text', '文本'), ('table', '表格'), ('image', '图片')),
default='text',
verbose_name="数据类型",
)
# 题注字段
fontnote = models.CharField(
max_length=256,
blank=True,
default="",
verbose_name="题注",
help_text="数据的题注说明"
)
# 内容字段 - 存储字符串、列表、字典
content = models.JSONField(verbose_name="内容", help_text="存储文本内容或二维表格数据或图片数据", default=default_json_value)
class Meta:
db_table = 'data_schemas'
verbose_name = "结构排序化数据"
verbose_name_plural = verbose_name
def __str__(self):
return f"结构排序化数据:({self.pk})"
# 影响域分析 - 隶属:轮次(不能是第一轮次)
class InfluenceArea(models.Model):
round = models.OneToOneField(to="Round", primary_key=True, db_constraint=False,
related_name="influence", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
class Meta:
db_table = 'round_influence_area'
verbose_name = "影响域分析"
verbose_name_plural = verbose_name
class InfluenceItem(CoreModel):
# 外键:影响域分析
influence = models.ForeignKey(InfluenceArea, db_constraint=False, related_name="influence_items", verbose_name="所属影响域分析",
on_delete=models.CASCADE, null=True, blank=True)
change_type = models.CharField(max_length=256, null=True, default="", verbose_name="更改类型", help_text="更改类型")
change_influ = models.TextField(max_length=2048, null=True, default="", verbose_name="影响域分析", help_text="影响域分析")
change_des = HTMLField(blank=True, null=True, verbose_name="更改内容描述")
effect_cases = models.JSONField(default=create_list, verbose_name="影响的用例key数组")
class Meta:
db_table = 'influence_item'
verbose_name = "影响域分析 - 行数据"
verbose_name_plural = verbose_name

View File

@@ -13,9 +13,9 @@ class DeleteSchema(Schema):
# 测试步骤输出schema
class CaseStepSchema(ModelSchema):
class Config:
class Meta:
model = CaseStep
model_fields = ["operation", 'expect', 'result', 'passed', 'case', 'id']
fields = ["operation", 'expect', 'result', 'passed', 'case', 'id']
# 测试用例的步骤输出schema输出isPassed和isExe转换后的
class CaseStepWithTransitionSchema(ModelSchema):
@@ -28,9 +28,9 @@ class CaseModelOutSchemaWithoutProblem(ModelSchema):
testStep: List[CaseStepWithTransitionSchema]
testType: str # 用例额外字段用于测试类型FT的标识给前端
class Config:
class Meta:
model = Case
model_exclude = ['project', 'round', 'dut', 'design', 'test', 'remark', 'sort']
exclude = ['project', 'round', 'dut', 'design', 'test', 'remark', 'sort']
# 输出case关联问题单
class CaseModelOutSchemaOrigin(ModelSchema):
@@ -39,9 +39,9 @@ class CaseModelOutSchemaOrigin(ModelSchema):
# 新增:关联的问题单
problem: Optional[ProblemModelOutSchema] = None
class Config:
class Meta:
model = Case
model_exclude = ['project', 'round', 'dut', 'design', 'test', 'remark', 'sort']
exclude = ['project', 'round', 'dut', 'design', 'test', 'remark', 'sort']
# 输出case关联问题单
class CaseModelOutSchema(ModelSchema):
@@ -52,9 +52,9 @@ class CaseModelOutSchema(ModelSchema):
# 2025年5月10日新增上级字段
test: Optional[TestDemandModelOutSchemaOrigin] = None
class Config:
class Meta:
model = Case
model_exclude = ['project', 'round', 'dut', 'design', 'test', 'remark', 'sort']
exclude = ['project', 'round', 'dut', 'design', 'test', 'remark', 'sort']
# 查询测试项
class CaseFilterSchema(Schema):
@@ -93,9 +93,9 @@ class CaseTreeInputSchema(Schema):
class CaseCreateOutSchema(ModelSchema):
level: Union[str, int]
class Config:
class Meta:
model = Case
model_exclude = ['remark', 'sort', 'project', 'round', 'dut', 'design']
exclude = ['remark', 'sort', 'project', 'round', 'dut', 'design']
# 新增接口schema
class CaseInputSchema(Schema):
@@ -126,6 +126,20 @@ class CaseCreateInputSchema(Schema):
# 新增时序图字段
timing_diagram: str = Field("", alias="timing_diagram")
# 批量新增测试用例
class OneCaseBatchCreateSchema(Schema):
parent_key: str
name: str
summarize: Optional[str] = ""
initialization: Optional[str] = ""
premise: Optional[str] = ""
sequence: Optional[str] = "" # 时序图
test_step: str
class BatchCreateCaseInputSchema(Schema):
project_id: int = Field(..., validation_alias=AliasChoices('project_id', 'projectId'))
cases: List[OneCaseBatchCreateSchema] = []
# 由demand创建case的输入Schema
class DemandNodeSchema(Schema):
project_id: int
@@ -150,7 +164,7 @@ class PersonReplaceSchema(Schema):
testPerson: str
monitorPerson: str
# 事件替换Schema
# 时间替换Schema
class ExetimeReplaceSchema(Schema):
selectRows: List[int] = None
exetime: str
exetime: List[str]

View File

@@ -1,5 +1,5 @@
from typing import Optional
from apps.project.models import Design
from apps.project.models import Design, JKDesignInfo
from ninja import Field, Schema, ModelSchema
from typing import List, Union
from pydantic import AliasChoices
@@ -23,17 +23,60 @@ class DesignFilterSchema(Schema):
# 2025年改为2个输出因为下级需要上级原始不再嵌套上级
class DesignModelOutSchemaOrigin(ModelSchema):
class Config:
class Meta:
model = Design
model_exclude = ['project', 'round', 'dut', 'remark', 'sort']
exclude = ['project', 'round', 'dut', 'remark', 'sort']
class DesignModelOutSchema(ModelSchema):
# 新增字段 - 上级的dut对象
dut: Optional[DutModelOutSchema] = None
is_bidirectional: bool = Field(False, alias='is_bidirectional')
forward_source: str = Field("")
forward_destination: str = Field("")
forward_description: str = Field("")
reverse_source: str = Field("")
reverse_destination: str = Field("")
reverse_description: str = Field("")
class Config:
class Meta:
model = Design
model_exclude = ['project', 'round', 'dut', 'remark', 'sort']
exclude = ['project', 'round', 'dut', 'remark', 'sort']
# ---------- 解析器方法 ----------
@staticmethod
def resolve_is_bidirectional(obj: Design) -> bool:
return obj.is_bidirectional
@staticmethod
def resolve_forward_source(obj: Design) -> str:
"""从 JKDesignInfo 正向记录中获取 source"""
info = obj.jkField.filter(direction=JKDesignInfo.Direction.FORWARD).first()
return info.source if info else ""
@staticmethod
def resolve_forward_destination(obj: Design) -> str:
info = obj.jkField.filter(direction=JKDesignInfo.Direction.FORWARD).first()
return info.destination if info else ""
@staticmethod
def resolve_forward_description(obj: Design) -> str:
info = obj.jkField.filter(direction=JKDesignInfo.Direction.FORWARD).first()
return info.description if info else ""
@staticmethod
def resolve_reverse_source(obj: Design) -> str:
info = obj.jkField.filter(direction=JKDesignInfo.Direction.REVERSE).first()
return info.source if info else ""
@staticmethod
def resolve_reverse_destination(obj: Design) -> str:
info = obj.jkField.filter(direction=JKDesignInfo.Direction.REVERSE).first()
return info.destination if info else ""
@staticmethod
def resolve_reverse_description(obj: Design) -> str:
info = obj.jkField.filter(direction=JKDesignInfo.Direction.REVERSE).first()
return info.description if info else ""
# 处理树状结构的schema
class DesignTreeReturnSchema(Schema):
@@ -66,11 +109,15 @@ class DesignCreateInputSchema(Schema):
demandType: str = Field(None, alias="demandType")
description: str = Field("", alias="description")
chapter: str = Field(None, alias='chapter')
# 接口独有的4个字段
source: str = Field('', alias='source')
to: str = Field('', alias='to')
# 接口类型包含
type: str = Field('', alias='type')
protocal: str = Field('', alias='protocal')
is_bidirectional: bool = Field(False, alias='is_bidirectional')
forward_source: str = Field("")
forward_destination: str = Field("")
forward_description: str = Field("")
reverse_source: str = Field("")
reverse_destination: str = Field("")
reverse_description: str = Field("")
class SingleDesignSchema(Schema):
ident: str = Field(None, alias="ident")

View File

@@ -5,9 +5,9 @@ from datetime import date
from pydantic import AliasChoices
class DutModelOutSchema(ModelSchema):
class Config:
class Meta:
model = Dut
model_exclude = ['project', 'round', 'remark', 'sort']
exclude = ['project', 'round', 'remark', 'sort']
class DutFilterSchema(Schema):
project_id: int = Field(None, alias='projectId')
@@ -56,9 +56,9 @@ class DutCreateOutSchema(ModelSchema):
effective_lines: Optional[Union[str, int]] = None
comment_lines: Optional[Union[str, int]] = None
class Config:
class Meta:
model = Dut
model_exclude = ['remark', 'sort', 'project', 'round']
exclude = ['remark', 'sort', 'project', 'round']
# 删除schema
class DeleteSchema(Schema):

View File

@@ -1,3 +1,5 @@
from pydantic import AliasChoices
from apps.project.models import Problem
from ninja import Field, Schema, ModelSchema
from typing import List, Optional
@@ -11,9 +13,9 @@ class ProblemModelOutSchema(ModelSchema):
related: Optional[bool] = Field(False) # 给前端反应是否为关联的问题单
hang: bool = Field(False) # 给前端反应是否是悬挂状态即没有关联case
class Config:
class Meta:
model = Problem
model_exclude = ['case', 'remark', 'sort']
exclude = ['case', 'remark', 'sort']
# 查询问题单
class ProblemFilterSchema(Schema):
@@ -53,13 +55,13 @@ class ProblemTreeInputSchema(Schema):
# 增加问题单
class ProblemCreateOutSchema(ModelSchema):
class Config:
class Meta:
model = Problem
model_exclude = ['remark', 'sort', 'case']
exclude = ['remark', 'sort', 'case']
# 更新新增schema
class ProblemCreateInputSchema(Schema):
project_id: int = Field(..., alias="projectId")
project_id: int = Field(..., validation_alias=AliasChoices('project_id', 'projectId'))
round_key: str = Field(None, alias="round")
dut_key: str = Field(None, alias="dut")
design_key: str = Field(None, alias="designDemand")

View File

@@ -1,5 +1,7 @@
from ninja.errors import HttpError
from apps.project.models import Project
from pyasn1_modules.rfc2315 import Data
from apps.project.models import Project, StuctSortData
from ninja import Schema, ModelSchema
from pydantic import field_validator
from typing import List, Optional
@@ -7,9 +9,9 @@ from typing import List, Optional
window_file_str = ['\\', '/', ':', '*', '?', '"', '<', '>', "|"]
class ProjectRetrieveSchema(ModelSchema):
class Config:
class Meta:
model = Project
model_exclude = ['update_datetime', 'create_datetime', 'remark']
exclude = ['update_datetime', 'create_datetime', 'remark']
class ProjectFilterSchema(Schema):
ident: Optional[str] = None
@@ -26,9 +28,9 @@ class ProjectFilterSchema(Schema):
class ProjectCreateInput(ModelSchema):
ident: str
class Config:
class Meta:
model = Project
model_exclude = ['remark', 'update_datetime', 'create_datetime', 'sort', 'id']
exclude = ['remark', 'update_datetime', 'create_datetime', 'sort', 'id']
@field_validator('ident')
@staticmethod
@@ -39,3 +41,31 @@ class ProjectCreateInput(ModelSchema):
class DeleteSchema(Schema):
ids: List[int]
# ~~~软件概述~~~
class DataSchema(Schema):
type: Optional[str] = "text"
fontnote: Optional[str] = ""
content: str | list[list[str]]
## 输入
class SoftSummarySchema(Schema):
id: int
data: list[DataSchema]
# ~~~软件接口图~~~
## 复用DataSchema
# ~~~静态软件项、静态硬件项、动态软件项、动态硬件项~~~
class StaticDynamicData(Schema):
id: int
category: str
table: list[list[str]]
fontnote: Optional[str] = ""
# ~~~环境差异性分析~~~
class EnvAnalysisSchema(Schema):
id: int
table: list[list[str]]
fontnote: Optional[str] = ""
description: Optional[str] = ""

View File

@@ -1,7 +1,7 @@
from typing import Optional
from ninja import Schema, ModelSchema
from pydantic import Field
from apps.project.models import Round
from apps.project.models import Round, InfluenceItem
# 输出树状信息的schema
class TreeReturnRound(Schema):
@@ -55,3 +55,21 @@ class CreateRoundInputSchema(ModelSchema):
fields_optional = ['best_condition_tem', 'best_condition_voltage',
'low_condition_tem', 'low_condition_voltage', 'typical_condition_tem',
'typical_condition_voltage' 'grade']
# influence_item return
class InfluenceItemOutSchema(ModelSchema):
class Meta:
model = InfluenceItem
fields = ['id', 'change_type', 'change_des', 'effect_cases', 'change_influ']
# influence input
class OneItemInputSchema(Schema):
change_type: str
change_des: Optional[str] = ""
effect_cases: Optional[list[str]] = []
change_influ: Optional[str] = ""
class InfluenceInputSchema(Schema):
id: int
round_key: str
item_list: list[OneItemInputSchema]

Some files were not shown because too many files have changed in this diff Show More