initial commit

This commit is contained in:
2025-04-29 18:09:00 +08:00
commit 4faed52de5
690 changed files with 13481 additions and 0 deletions

5
.env Normal file
View File

@@ -0,0 +1,5 @@
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)'

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
/dist
/venv
/.idea
/build
/static
*.zip
/uploads
.idea
/打包后配置
/packagesInitialize

32
.idea/cdtestplant_v1.iml generated Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="cdtestplant_v1/settings.py" />
<option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<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="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/dist/run/templates" />
</list>
</option>
</component>
</module>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="cdtestplant_v1" uuid="2b73fc88-303c-4aa1-a8da-50bba5915b7d">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/chengdu_test_plant_v1</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

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

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cdtestplant_v1.iml" filepath="$PROJECT_DIR$/.idea/cdtestplant_v1.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# change_log
## 内外V0.0.1版本
2024年7月3日 - V0.0.1版本首次导入内网并进行数据库迁移和部署
## 外V0.0.2版本
## 外V0.0.3版本

0
apps/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class CreatedocumentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.createDocument'
# 在app准备好时候执行的代码以后放async、signal等
def ready(self):
pass

View File

@@ -0,0 +1,12 @@
# 导入所有本目录控制器,为了自动导入
from apps.createDocument.controllers.dg import GenerateControllerDG
from apps.createDocument.controllers.sm import GenerateControllerSM
from apps.createDocument.controllers.jl import GenerateControllerJL
from apps.createDocument.controllers.bg import GenerateControllerBG
from apps.createDocument.controllers.wtd import GenerateControllerWtd
from apps.createDocument.controllers.hsm import GenerateControllerHSM
from apps.createDocument.controllers.hjl import GenerateControllerHJL
# 给外部导入
__all__ = ['GenerateControllerDG', 'GenerateControllerSM', 'GenerateControllerJL', 'GenerateControllerBG',
'GenerateControllerWtd', 'GenerateControllerHSM', 'GenerateControllerHJL']

View File

@@ -0,0 +1,724 @@
from datetime import date, timedelta
from pathlib import Path
from ninja_extra import api_controller, ControllerBase, route
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.db.models import Q
from docxtpl import DocxTemplate
from typing import Optional
from docx import Document
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
# 导入模型
from apps.project.models import Project, Dut, TestDemand, Problem
# 工具类函数
from apps.createDocument.extensions import util
from utils.chen_response import ChenResponse
from apps.createDocument.extensions.util import create_bg_docx, get_round1_problem
from utils.util import get_str_dict, get_list_dict, create_problem_grade_str, create_str_testType_list, \
create_demand_summary, create_problem_type_str, create_problem_table, create_problem_type_table, get_str_abbr
# 根据轮次生成测评内容文档context
from apps.createDocument.extensions.content_result_tool import create_round_context
from apps.createDocument.extensions.zhui import create_bg_round1_zhui
from apps.createDocument.extensions.solve_problem import create_one_problem_dit
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.createDocument.extensions.documentTime import DocTime
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
# @api_controller("/generateBG", tags=['生成报告文档系列'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generateBG", tags=['生成报告文档系列'])
class GenerateControllerBG(ControllerBase):
logger = GenerateLogger('测评报告')
# important删除之前的文件
@route.get('/create/deleteBGDocument', url_name='delete-bg-document')
def delete_bg_document(self, id: int):
project_path_str = project_path(id)
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/bg'
delete_dir_files(save_path)
@route.get("/create/techyiju", url_name="create-techyiju")
@transaction.atomic
def create_techyiju(self, id: int):
project_obj = get_object_or_404(Project, id=id)
duties_qs = project_obj.pdField.filter(Q(type='XQ') | Q(type='SJ') | Q(type='XY'))
std_documents = []
for duty in duties_qs:
one_duty = {'doc_name': duty.name, 'ident_version': duty.ref + '-' + duty.version,
'publish_date': duty.release_date, 'source': duty.release_union}
std_documents.append(one_duty)
# 添加大纲到这里
## 判断是否为鉴定
doc_name = f'{project_obj.name}软件测评大纲'
if project_obj.report_type == '9':
doc_name = f'{project_obj.name}软件鉴定测评大纲'
# 时间控制类
timer = DocTime(id)
# 这里大纲版本升级如何处理
dg_duty = {'doc_name': doc_name, 'ident_version': f'PT-{project_obj.ident}-TO-1.00',
'publish_date': timer.dg_cover_time, 'source': project_obj.test_unit}
std_documents.append(dg_duty)
# 需要添加说明、记录
sm_duty = {'doc_name': f'{project_obj.name}软件测试说明', 'ident_version': f'PT-{project_obj.ident}-TD-1.00',
'publish_date': timer.sm_cover_time, 'source': project_obj.test_unit}
jl_duty = {'doc_name': f'{project_obj.name}软件测试记录', 'ident_version': f'PT-{project_obj.ident}-TN',
'publish_date': timer.jl_cover_time, 'source': project_obj.test_unit}
# 循环所有轮次,除了第一轮
std_documents.extend([sm_duty, jl_duty])
rounds = project_obj.pField.exclude(key='0')
name_list = ['', '', '', '', '', '', '', '', '', '']
index = 1
for r in rounds:
hsm_duty = {'doc_name': f'{project_obj.name}软件第{name_list[index]}轮测试说明',
'ident_version': f'PT-{project_obj.ident}-TD{str(index + 1)}-1.00',
'publish_date': r.beginTime, 'source': project_obj.test_unit}
hjl_duty = {'doc_name': f'{project_obj.name}软件第{name_list[index]}轮测试记录',
'ident_version': f'PT-{project_obj.ident}-TN{str(index + 1)}',
'publish_date': r.endTime, 'source': project_obj.test_unit}
std_documents.extend([hsm_duty, hjl_duty])
index += 1
# 生成二级文档
context = {
'std_documents': std_documents
}
return create_bg_docx("技术依据文件.docx", context, id)
# 测评地点和时间接口
@route.get('/create/timeaddress')
@transaction.atomic
def create_timeaddress(self, id: int):
timer = DocTime(id)
context = timer.bg_address_time()
return create_bg_docx('测评时间和地点.docx', context, id)
# 在报告生成多个版本被测软件基本信息
@route.get('/create/baseInformation', url_name='create-baseInformation')
def create_information(self, id: int):
project_obj = get_object_or_404(Project, id=id)
languages = get_list_dict('language', project_obj.language)
language_list = []
for language in languages:
language_list.append(language.get('ident_version'))
# 获取轮次
rounds = project_obj.pField.all()
round_list = []
for r in rounds:
round_dict = {}
# 获取SO的dut
so_dut: Dut = r.rdField.filter(type='SO').first()
if so_dut:
round_dict['version'] = so_dut.version
round_dict['line_count'] = int(so_dut.total_lines)
round_dict['effective_line'] = int(so_dut.effective_lines)
round_list.append(round_dict)
context = {
'project_name': project_obj.name,
'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'),
'language': "\a".join(language_list),
'recv_date': project_obj.beginTime.strftime("%Y-%m-%d"),
'dev_unit': project_obj.dev_unit,
'version_info': round_list
}
return create_bg_docx('被测软件基本信息.docx', context, id)
# 生成测评完成情况
@route.get('/create/completionstatus', url_name='create-completionstatus')
def create_completionstatus(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 找到第一轮轮次对象、第二轮轮次对象
round1 = project_obj.pField.filter(key='0').first()
# 第一轮测试项个数
round1_demand_qs = round1.rtField.all()
# 第一轮用例个数
round1_case_qs = round1.rcField.all()
# 这部分找出第一轮的所有测试类型,输出字符串,并排序
test_type_set: set = set()
for case in round1_case_qs:
demand: TestDemand = case.test
test_type_set.add(demand.testType)
round1_testType_list = list(map(lambda x: x['ident_version'], get_list_dict('testType', list(test_type_set))))
# 这里找出第一轮,源代码被测件,并获取版本
so_dut = round1.rdField.filter(type='SO').first()
so_dut_verson = "$请添加第一轮的源代码信息$"
if so_dut:
so_dut_verson = so_dut.version
# 这里找出除第一轮的其他轮次
rounds = project_obj.pField.exclude(key='0')
rounds_str_chinese = ['', '', '', '', '', '', '', '', '', '']
round_list = []
for r in rounds:
# 找所属dut的so-dut
so_dut = r.rdField.filter(type='SO').first()
# 找出上一轮dut的so-dut
last_problem_count = Problem.objects.filter(case__round__key=str(int(r.key) - 1)).distinct().count()
current_round_problem_count = Problem.objects.filter(case__round__key=r.key).distinct().count()
if current_round_problem_count > 0:
current_round_description = f'引入新问题{current_round_problem_count}'
else:
current_round_description = '经测试软件更改正确,并且未引入新的问题'
r_dict = {
'version': so_dut.version if so_dut else '$请添加该轮次源代码信息$',
'round_index': rounds_str_chinese[int(r.key)],
'last_problem_count': last_problem_count,
'current_round_description': current_round_description,
'start_year': r.beginTime.year,
'start_month': r.beginTime.month,
'end_year': (r.beginTime + timedelta(days=4)).year, # 这里只是简单+4有待商榷
'end_month': (r.beginTime + timedelta(days=4)).month,
}
round_list.append(r_dict)
# 这部分找到第一轮的问题
problem_qs = get_round1_problem(project_obj)
context = {
'is_JD': True if project_obj.report_type == '9' else False,
'project_name': project_obj.name,
'start_time_year': project_obj.beginTime.year,
'start_time_month': project_obj.beginTime.month,
'round1_case_count': round1_case_qs.count(),
'round1_demand_count': round1_demand_qs.count(),
'round1_testType_str': ''.join(round1_testType_list),
'testType_count': len(round1_testType_list),
'round1_version': so_dut_verson,
'round1_problem_count': len(problem_qs),
'end_time_year': date.today().year,
'end_time_month': date.today().month,
'round_list': round_list
}
# 注入时间
timer = DocTime(id)
context.update(**timer.bg_completion_situation())
return create_bg_docx('测评完成情况.docx', context, id)
# 生成综述
@route.get('/create/summary', url_name='create-summary')
def create_summary(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 找出所有问题单
problem_qs = project_obj.projField.all()
problem_grade_dict = {}
problem_type_dict = {}
# 建议问题统计
problem_suggest_count = 0
problem_suggest_solved_count = 0
for problem in problem_qs:
grade_key: str = get_str_dict(problem.grade, 'problemGrade')
type_key: str = get_str_dict(problem.type, 'problemType')
# 问题等级字典-计数
if grade_key in problem_grade_dict.keys():
problem_grade_dict[grade_key] += 1
else:
problem_grade_dict[grade_key] = 1
# 问题类型字典-计数
if type_key in problem_type_dict.keys():
problem_type_dict[type_key] += 1
else:
problem_type_dict[type_key] = 1
# 建议问题统计
if problem.grade == '3':
problem_suggest_count += 1
if problem.status == '1':
problem_suggest_solved_count += 1
problem_grade_list = []
problem_type_list = []
for key, value in problem_grade_dict.items():
problem_grade_list.append("".join([f"{key}问题", f"{value}"]))
for key, value in problem_type_dict.items():
problem_type_list.append("".join([f"{key}", f"{value}"]))
# 用来生成建议问题信息
if problem_suggest_count > 0 and problem_suggest_count - problem_suggest_solved_count > 0:
all_str = (f"测评过程中提出了{problem_suggest_count}个建议改进,"
f"其中{problem_suggest_solved_count}个建议改进已修改,"
f"剩余{problem_suggest_count - problem_suggest_solved_count}个未修改并经总体单位认可同意")
elif problem_suggest_count > 0 and problem_suggest_count - problem_suggest_solved_count == 0:
all_str = (f"测评过程中提出了{problem_suggest_count}个建议改进,"
f"全部建议问题已修改")
else:
all_str = f"测评过程中未提出建议项。"
context = {
'problem_count': problem_qs.count(),
'problem_grade_str': "".join(problem_grade_list),
'problem_type_str': ''.join(problem_type_list),
'all_str': all_str,
}
return create_bg_docx('综述.docx', context, id)
# 生成测试内容和结果[报告非常关键的一环-大模块]
@route.get('/create/contentandresults_1', url_name='create-contentandresults_1')
@transaction.atomic
def create_content_results_1(self, id: int):
project_obj = get_object_or_404(Project, id=id)
project_ident = project_obj.ident
# ~~~~首轮信息~~~~
round1 = project_obj.pField.filter(key='0').first() # !warning轮次1对象
# 1.处理首轮文档名称,新修改,这里取全部轮次的文档内容
doc_list = []
round1_duts = project_obj.pdField.filter(Q(type='SJ') | Q(type='XQ') | Q(type='XY'))
index = 1
for dut in round1_duts:
dut_dict = {
'name': dut.name,
'ident': dut.ref,
'version': dut.version,
'index': index
}
doc_list.append(dut_dict)
index += 1
# 2.处理首轮文档问题的统计 - 注意去重
problems = project_obj.projField.all().distinct() # !important:大变量-项目所有问题
problems_r1 = problems.filter(case__round__key='0') # !important:大变量-首轮的所有问题
problems_doc_r1 = problems_r1.filter(case__test__testType='8') # 第一轮所有文档问题
# 3.第一轮代码审查问题统计/版本
source_r1_dut = round1.rdField.filter(type='SO').first() # !warning:小变量-第一轮源代码对象
program_r1_problems = problems_r1.filter(case__test__testType='2')
# 4.第一轮代码走查问题统计/版本
zou_r1_problems = problems_r1.filter(case__test__testType='3')
# 找下是否存在代码走查测试项
r1_demand_qs = round1.rtField.filter(testType='3')
has_zou = True if r1_demand_qs.count() > 0 else False
# 5.第一轮静态分析问题统计
static_problems = problems_r1.filter(case__test__testType='15')
# 6.第一轮动态测试用例个数(动态测试-非静态分析、文档审查、代码审查、代码走查4个)
case_r1_qs = round1.rcField.filter(~Q(test__testType='2'), ~Q(test__testType='3'), ~Q(test__testType='8'),
~Q(test__testType='15'),
round__key='0') # !warning:中变量-第一轮动态测试用例qs
testType_list, testType_count = create_str_testType_list(case_r1_qs)
## 动态测试(第一轮)各个类型测试用例执行表/各个测试需求表
demand_r1_dynamic_qs = round1.rtField.filter(~Q(testType='2'), ~Q(testType='3'), ~Q(testType='8'),
~Q(testType='15')) # !warning:中变量:第一轮动态测试的测试项
summary_r1_demand_info, summry_r1_demandType_info = create_demand_summary(demand_r1_dynamic_qs, project_ident)
# N.第一轮所有动态问题统计
problems_dynamic_r1 = problems_r1.filter(~Q(case__test__testType='2'), ~Q(case__test__testType='3'),
~Q(case__test__testType='8'),
~Q(case__test__testType='15')) # !critical:大变量:第一轮动态问题单qs
problem_dynamic_r1_type_str = create_problem_type_str(problems_dynamic_r1)
problem_dynamic_r1_grade_str = create_problem_grade_str(problems_dynamic_r1)
context = {
'project_name': project_obj.name,
'doc_list': doc_list,
'r1_doc_problem_count': problems_doc_r1.count(),
'r1_doc_problem_str':
f"{',其中' + create_problem_grade_str(problems_doc_r1) if problems_doc_r1.count() > 0 else '即未发现问题'}",
'r1_version': source_r1_dut.version if source_r1_dut else "未录入首轮版本信息",
'r1_program_problem_count': program_r1_problems.count(),
'r1_program_problem_str':
f'{",其中" + create_problem_grade_str(program_r1_problems) if program_r1_problems.count() > 0 else "即未发现问题"}',
'r1_zou_problem_count': zou_r1_problems.count(),
'r1_zou_problem_str': f'{",其中" + create_problem_grade_str(zou_r1_problems) if zou_r1_problems.count() > 0 else "即未发现问题"}',
'has_zou': has_zou,
'r1_static_problem_count': static_problems.count(),
'r1_static_problem_str': f"{',其中' + create_problem_grade_str(static_problems) if static_problems.count() > 0 else '即未发现问题'}",
'r1_case_count': case_r1_qs.count(),
'r1_case_testType': "".join(testType_list),
'r1_case_testType_count': testType_count,
'r1_problem_counts': len(problems_dynamic_r1),
'r1_exe_info_all': summary_r1_demand_info,
'r1_exe_info_type': summry_r1_demandType_info,
'r1_dynamic_problem_str': problem_dynamic_r1_type_str,
'r1_dynamic_problem_grade_str': problem_dynamic_r1_grade_str,
}
return create_bg_docx("测试内容和结果_第一轮次.docx", context, id)
# 查询除第一轮以外,生成其他轮次测试内容和结果
@route.get('/create/contentandresults_2', url_name='create-contentandresults_2')
@transaction.atomic
def create_content_results_2(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 查询除第一轮,其他有几轮
round_qs = project_obj.pField.filter(~Q(key='0'))
round_str_list = [item.key for item in round_qs]
# 每个轮次都需要生成一个测试内容和标题
project_path_str = project_path(id)
for round_str in round_str_list:
context = create_round_context(project_obj, round_str)
template_path = Path.cwd() / 'media' / project_path_str / 'form_template' / 'bg' / '测试内容和结果_第二轮次.docx'
doc = DocxTemplate(template_path)
doc.render(context)
try:
doc.save(
Path.cwd() / "media" / project_path_str / "output_dir/bg" / f"测试内容和结果_第{context['round_id']}轮次.docx")
except PermissionError:
ChenResponse(code=400, status=400, message='您已打开生成文件,请关闭后再试...')
# 软件问题统计
@route.get('/create/problem_statistics')
@transaction.atomic
def create_problem_statistics(self, id: int):
project_obj = get_object_or_404(Project, id=id)
problems = project_obj.projField.all().distinct() # 项目所有问题单
context = {
'closed_count': problems.filter(status='1').count(),
'noclosed_count': problems.count() - problems.filter(status='1').count(),
'problem_table': create_problem_table(problems),
'problem_table_2': create_problem_type_table(problems)
}
return create_bg_docx("软件问题统计.docx", context, id)
# 测试有效性充分性说明
@route.get('/create/effect_and_adquacy', url_name='create-effect_and_adquacy')
@transaction.atomic
def create_effect_and_adquacy(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 判断是否为鉴定
is_JD = False
if project_obj.report_type == '9':
is_JD = True
# 统计测试项数量
demand_qs = project_obj.ptField
# 统计用例个数
case_qs = project_obj.pcField
# 测试用例的类型统计个数
testType_list, testType_count = create_str_testType_list(case_qs.all())
# 问题单总个数
problem_qs = project_obj.projField
context = {
'project_name': project_obj.name,
'demand_count': demand_qs.count(),
'case_count': case_qs.count(),
'testType_list': "".join(testType_list),
'testType_count': testType_count,
'problem_count': problem_qs.count(),
'is_JD': is_JD,
}
return create_bg_docx('测试有效性充分性说明.docx', context, id)
# 需求指标符合性情况
@route.get('/create/demand_effective', url_name='create-demand_effective')
@transaction.atomic
def create_demand_effective(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 查询所有需求规格说明的 - 设计需求
round1_design_qs = project_obj.psField.filter(round__key='0', dut__type='XQ') # qs:第一轮需求文档的设计需求
# 将第一轮需求文档名称
dut_name = f"{project_obj.name}软件需求规格说明》"
data_list = []
design_index = 1
for design in round1_design_qs:
# 如果为“/”则写为隐含需求
if design.chapter.strip() == '/':
design_dict = {'source': "隐含需求"}
else:
design_dict = {'source': "".join([dut_name, design.name, ':', design.chapter])}
# 将设计需求描述筛入
rich_parser = RichParser(design.description)
p_list = rich_parser.get_final_p_list()
design_dict['description'] = '\a'.join(p_list)
# 找出其中所有demand
demand_qs = design.dtField.all()
if not demand_qs.exists():
design_dict['demands'] = '未关联测试项'
else:
demand_list = []
index = 0
for demand in demand_qs:
index += 1
demand_abbr = get_str_abbr(demand.testType, 'testType')
demand_list.append(f'{index}、XQ_{demand_abbr}_{demand.ident}-{demand.name}')
design_dict['demands'] = '\a'.join(demand_list)
# 通过还是未通过
design_dict['pass'] = '通过'
design_dict['index'] = design_index
data_list.append(design_dict)
design_index += 1
# ~~~~指标符合性表~~~~
data_yz_list = []
# qs:第一轮需求文档的设计需求
has_YZ = False
round1_design_yz_qs = project_obj.psField.filter(round__key='0', dut__type='YZ')
if round1_design_yz_qs.exists():
has_YZ = True
# 如果有研制总要求的dut继续
for design in round1_design_yz_qs:
rich_parser2 = RichParser(design.description)
p_list = rich_parser2.get_final_p_list()
design_dict = {'yz_des': "".join([design.chapter, '章节:', design.name, '\a', '\a'.join(p_list)])}
# 找出其中所有demand
demand_qs = design.dtField.all()
if not demand_qs.exists():
design_dict['demands'] = '未关联测评大纲条款'
else:
# 大纲条款的列表
demand_list = []
demand_step_list = []
index = 0
for demand in demand_qs:
index += 1
demand_list.append(f'{index}{demand.ident}-{demand.name}')
# 测试需求步骤的列表
step_list = []
for step in demand.testQField.all():
step_list.append(step.subName)
demand_step_list.append('\a'.join(step_list))
design_dict['demands'] = '\a'.join(demand_list)
design_dict['steps'] = '\a'.join(demand_step_list)
# 通过还是未通过
design_dict['pass'] = '通过'
data_yz_list.append(design_dict)
# 处理没有steps字段
if 'steps' not in design_dict:
design_dict['steps'] = '该设计需求未关联测评大纲条款'
context = {
'data_list': data_list,
'data_yz_list': data_yz_list,
'has_YZ': has_YZ,
}
return create_bg_docx('需求指标符合性情况.docx', context, id)
# 软件质量评价
@route.get('/create/quality_evaluate', url_name='create-quality_evaluate')
@transaction.atomic
def create_quality_evaluate(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 找出最后一轮
rounds = project_obj.pField.order_by('-key') # qs轮次
last_dut_so: Optional[Dut] = None
for round in rounds:
# 查询其源代码dut
dut_so = round.rdField.filter(type='SO').first()
if dut_so:
last_dut_so = dut_so
break
# 计算千行缺陷率
problem_count = project_obj.projField.count()
# 如果没有轮次信息则返回错误
if not last_dut_so:
return ChenResponse(code=400, status=400, message='您还未创建轮次,请进入工作区创建')
# 计算注释率
## 总行数
total_lines = int(last_dut_so.total_lines)
## 有效注释行
effective_comment_lines = int(last_dut_so.comment_lines)
comment_ratio = (effective_comment_lines / total_lines) * 100
context = {
'last_version': last_dut_so.version, # 最后轮次代码版本
'comment_percent': format(comment_ratio, '.4f'), # 最后轮次代码注释率
'qian_comment_rate': format(problem_count / int(last_dut_so.total_lines) * 1000, '.4f'),
'avg_function_lines': "XXXX",
'avg_cyclomatic': 'XXXX',
'avg_fan_out': 'XXXX',
}
# 判断是否有metrics一对一模型关联
if hasattr(last_dut_so, 'metrics'):
context['avg_function_lines'] = str(last_dut_so.metrics.avg_function_lines)
context['avg_cyclomatic'] = str(last_dut_so.metrics.avg_cyclomatic)
context['avg_fan_out'] = str(last_dut_so.metrics.avg_fan_out)
return create_bg_docx('软件质量评价.docx', context, id)
# 软件总体结论
@route.get('/create/entire', url_name='create-entire')
@transaction.atomic
def create_entire(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 是否鉴定
is_JD = False
if project_obj.report_type == '9':
is_JD = True
# 找出最后一轮并且有源代码的dut
rounds = project_obj.pField.order_by('-key') # qs轮次
last_dut_so: Optional[Dut] = None
for round in rounds:
# 查询其源代码dut
dut_so = round.rdField.filter(type='SO').first()
if dut_so:
last_dut_so = dut_so
break
# 找出所有被测件协议XY、需求规格说明XQ、设计说明SJ
duties_qs: list[Dut] = project_obj.pdField.filter(Q(type='XQ') | Q(type='SJ') | Q(type='XY'))
# ***Inspect-start***
if not last_dut_so:
self.logger.model = '测评报告'
self.logger.write_warning_log('总体结论', f'项目没创建轮次,请检查')
return None
# ***Inspect-end***
context = {
'name': project_obj.name,
'last_version': last_dut_so.version,
'is_JD': is_JD,
'dut_list': [
{
'index': index + 1,
'name': dut_single.name,
'ref': dut_single.ref,
'version': dut_single.version,
} for index, dut_single in enumerate(duties_qs)
],
'last_dut_so_ref': last_dut_so.ref,
}
return create_bg_docx('总体结论.docx', context, id)
# 研总需求追踪 - 注意生成每个轮次的追踪
@route.get('/create/yzxq_track', url_name='create-yzxq_track')
@transaction.atomic
def create_yzxq_track(self, id: int):
project_obj = get_object_or_404(Project, id=id)
# 是否是鉴定的变量,如果为鉴定,则需要研总的追踪
is_JD = False
if project_obj.report_type == '9':
is_JD = True
# 查询多少个轮次
round_count = project_obj.pField.count()
round_str_list = [str(i) for i in range(round_count)]
# 生成研总的design_list
design_list_all = []
for round_str in round_str_list:
# 找寻轮次里面源代码版本
dut_version = 'XXX'
dut_so = Dut.objects.filter(round__key=round_str, type='SO').first()
if dut_so:
dut_version = dut_so.version
if is_JD:
design_list_yz = create_bg_round1_zhui(project_obj, dut_str='YZ', round_str=round_str)
one_table_dict = {
'design_list': design_list_yz,
'version': 'V' + dut_version,
'title': '研制总要求'
}
design_list_all.append(one_table_dict)
design_list_xq = create_bg_round1_zhui(project_obj, dut_str='XQ', round_str=round_str)
one_table_dict_xq = {
'design_list': design_list_xq,
'version': 'V' + dut_version,
'title': '需求规格说明'
}
design_list_all.append(one_table_dict_xq)
else:
design_list_xq = create_bg_round1_zhui(project_obj, dut_str='XQ', round_str=round_str)
one_table_dict_xq = {
'design_list': design_list_xq,
'version': 'V' + dut_version,
'title': '需求规格说明'
}
design_list_all.append(one_table_dict_xq)
context = {
'design_list_all': design_list_all,
}
# 手动渲染tpl文档
project_path_str = project_path(id)
input_file = Path.cwd() / 'media' / project_path_str / 'form_template' / 'bg' / '研总需归追踪.docx'
temporary_file = Path.cwd() / 'media' / project_path_str / 'form_template' / 'bg' / 'temporary' / '研总需归追踪_temp.docx'
out_put_file = Path.cwd() / 'media' / project_path_str / 'output_dir' / 'bg' / '研总需归追踪.docx'
doc = DocxTemplate(input_file)
doc.render(context)
doc.save(temporary_file)
# 通过docx合并单元格
if temporary_file.is_file():
try:
docu = Document(temporary_file)
# 循环找到表格
for table in docu.tables:
util.merge_all_cell(table)
# 储存到合适位置
docu.save(out_put_file)
return ChenResponse(code=200, status=200, message='文档生成成功...')
except PermissionError:
return ChenResponse(code=400, status=400, message='请检查文件是否打开,如果打开则关闭...')
else:
return ChenResponse(code=400, status=400, message='中间文档未找到,请检查你模版是否存在...')
# 生成问题汇总表
@route.get('/create/problems_summary', url_name='create-problem_summary')
@transaction.atomic
def create_problem_summary(self, id: int):
tpl_doc = Path.cwd() / "media" / project_path(id) / "form_template" / "bg" / "问题汇总表.docx"
doc = DocxTemplate(tpl_doc)
project_obj = get_object_or_404(Project, id=id)
problem_prefix = "_".join(['PT', project_obj.ident])
problems = project_obj.projField
# 先查询有多少轮次
round_count = project_obj.pField.count()
round_str_list = [str(x) for x in range(round_count)]
data_list = []
for round_str in round_str_list:
# 查询所属当前轮次的SO-dut
so_dut = Dut.objects.filter(round__key=round_str, type='SO').first()
round_dict = {
'static': [],
'dynamic': [],
'version': so_dut.version if so_dut else "v1.0",
}
# 找出轮次中静态问题
r1_static_problems = problems.filter(case__round__key=round_str,
case__test__testType__in=['2', '3', '8', '15']).distinct()
for problem in r1_static_problems:
problem_dict = create_one_problem_dit(problem, problem_prefix, doc)
round_dict['static'].append(problem_dict)
# 找出轮次中动态问题
r1_dynamic_problems = problems.filter(case__round__key=round_str).exclude(
case__test__testType__in=['2', '3', '8', '15']).distinct()
for problem in r1_dynamic_problems:
problem_dict = create_one_problem_dit(problem, problem_prefix, doc)
round_dict['dynamic'].append(problem_dict)
data_list.append(round_dict)
context = {
'data_list': data_list
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/bg" / "问题汇总表.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 生成摸底清单
@route.get('/create/modi_list', url_name='create-modi-list')
@transaction.atomic
def create_modi_list(self, id: int):
tpl_doc = Path.cwd() / "media" / project_path(id) / "form_template" / "bg" / "摸底清单.docx"
doc = DocxTemplate(tpl_doc)
project_obj = get_object_or_404(Project, id=id)
# 查询所有轮次“摸底测试”的测试项
demands_qs = project_obj.ptField.all()
modi_list = []
for demand in demands_qs:
one_modi = {}
testType_str = get_str_dict(demand.testType, 'testType')
if "摸底" in testType_str:
# 1.找到设计需求章节号以及描述
design = demand.design
one_modi['source'] = f"{design.chapter}-{design.name}" if design.chapter != '/' else "隐含需求"
one_modi['desc'] = "\a".join(RichParser(design.description).get_final_p_list())
# 2.找所有的case
case_qs = demand.tcField.all()
one_modi['result'] = []
for case in case_qs:
# 找case的步骤
for step in case.step.all():
if step.passed == '1': # 只获取通过的
one_modi['result'].append("\a".join(RichParser(step.result).get_final_p_list()))
modi_list.append(one_modi)
# 渲染上下文
context = {
'modi_list': modi_list,
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir/bg" / "摸底清单.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))

View File

@@ -0,0 +1,784 @@
from ninja.errors import HttpError
from ninja_extra import ControllerBase, api_controller, route
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
from django.db import transaction
from django.db.models import Q
from docxtpl import DocxTemplate
from pathlib import Path
from utils.chen_response import ChenResponse
# 导入数据库ORM
from apps.project.models import Project, Contact, Abbreviation
from apps.dict.models import Dict
# 导入工具函数
from utils.util import get_str_dict, get_list_dict, get_testType, get_ident, get_str_abbr
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from utils.util import MyHTMLParser_p
from django.shortcuts import get_object_or_404
from django.forms.models import model_to_dict
from apps.createDocument.extensions.util import create_dg_docx
from apps.createDocument.extensions.parse_rich_text import RichParser
from apps.createDocument.extensions.documentTime import DocTime
from utils.path_utils import project_path
# 记录生成日志
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
# 导入mixins-处理文档片段
from apps.createDocument.extensions.mixins import FragementToolsMixin
# @api_controller("/generate", tags=['生成大纲文档'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generate", tags=['生成大纲文档'])
class GenerateControllerDG(ControllerBase, FragementToolsMixin):
logger = GenerateLogger('测评大纲')
@route.get("/create/testdemand", url_name="create-testdemand")
@transaction.atomic
def create_testdemand(self, id: int): # type:ignore
"""目前生成第一轮测试项"""
tplTestDemandGenerate_path = Path.cwd() / "media" / project_path(id) / "form_template" / "dg" / "测试项及方法.docx"
doc = DocxTemplate(tplTestDemandGenerate_path)
# 获取指定的项目对象
project_qs = get_object_or_404(Project, id=id)
# 先查询dict字典查出总共有多少个testType
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 _ in range(1, test_type_len + 1)]
# 查出第一轮所有testdemand
project_round_one = project_qs.pField.filter(key=0).first()
testDemand_qs = project_round_one.rtField.all()
# 遍历第一轮测试项默认是ID排序
for single_qs in testDemand_qs:
type_index = type_number_list.index(int(single_qs.testType))
# 先查询其testDemandContent信息
content_list = []
for (index, content) in enumerate(single_qs.testQField.all()):
content_dict = {
"index": index + 1,
"rindex": str(index + 1).rjust(2, '0'),
"subName": content.subName,
# 修改遍历content下面的stepcontent变量是TestDemandContent表
"subStep": [
{'index': index + 1, 'operation': step_obj.operation, 'expect': step_obj.expect}
for (index, step_obj) in enumerate(content.testStepField.all())
],
}
content_list.append(content_dict)
# 查询测试项中testMethod
testmethod_str = ''
for dict_item_qs in Dict.objects.get(code="testMethod").dictItem.all():
for tm_item in single_qs.testMethod:
if tm_item == dict_item_qs.key:
testmethod_str += dict_item_qs.title + " "
# 富文本解析
# ***Inspect-start检查设计需求的描述是否为空***
if single_qs.design.description == '':
design_info = single_qs.design.ident + '-' + single_qs.design.name
self.logger.write_warning_log('测试项', f'设计需求中的描述为空,请检查 -> {design_info}')
# ***Inspect-end***
html_parser = RichParser(single_qs.design.description)
desc_list = html_parser.get_final_list(doc)
# 查询关联design以及普通design
doc_list = [{'dut_name': single_qs.dut.name, 'design_chapter': single_qs.design.chapter,
'design_name': single_qs.design.name}]
for relate_design in single_qs.otherDesign.all():
ddict = {'dut_name': relate_design.dut.name, 'design_chapter': relate_design.chapter,
'design_name': relate_design.name}
doc_list.append(ddict)
# 组装单个测试项
testdemand_dict = {
"name": single_qs.name,
"key": single_qs.key,
"ident": get_ident(single_qs),
"priority": get_str_dict(single_qs.priority, "priority"),
"doc_list": doc_list,
"design_description": desc_list,
"test_demand_content": content_list,
"testMethod": testmethod_str,
"adequacy": single_qs.adequacy.replace("\n", "\a"),
"testDesciption": single_qs.testDesciption.replace("\n", "\a") # 测试项描述
}
list_list[type_index].append(testdemand_dict)
# 定义渲染context字典
context = {
"project_name": project_qs.name
}
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)
# 排序1测试类型排序
output_list = sorted(output_list, key=(lambda x: x["sort"]))
context["data"] = output_list
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / "测试项及方法.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
@route.get("/create/yiju", url_name='create-yiju')
@transaction.atomic
def create_yiju(self, id: int):
# 先找出所属项目
project_qs = get_object_or_404(Project, id=id)
# 找出该项目的真实依据文件qs
yiju_list = get_list_dict('standard', project_qs.standard)
context = {
'std_documents': yiju_list
}
return create_dg_docx('标准依据文件.docx', context, id)
@route.get("/create/techyiju", url_name='create-techyiju')
@transaction.atomic
def create_techyiju(self, id: int):
# 找出所属项目
project_qs = get_object_or_404(Project, id=id)
# 根据项目找出被测件-只找第一轮次
duties_qs = project_qs.pdField.filter(Q(type='XQ') | Q(type='SJ') | Q(type='XY') | Q(type='YZ')).filter(
round__key='0')
# 先定义个字典
std_documents = []
for duty in duties_qs:
one_duty = {'doc_name': duty.name, 'ident_version': duty.ref + '-' + duty.version,
'publish_date': duty.release_date, 'source': duty.release_union}
std_documents.append(one_duty)
# 生成二级文档
context = {
'std_documents': std_documents
}
return create_dg_docx('技术依据文件.docx', context, id)
@route.get("/create/contact", url_name='create-contact')
@transaction.atomic
def create_contact(self, id: int):
# 先找出所属项目
project_qs = get_object_or_404(Project, id=id)
contact_dict = model_to_dict(project_qs,
fields=['entrust_unit', 'entrust_contact', 'entrust_contact_phone', 'dev_unit',
'dev_contact', 'dev_contact_phone', 'test_unit', 'test_contact',
'test_contact_phone'])
# 根据entrust_unit、dev_unit、test_unit查找Contact中地址信息
entrust_addr = Contact.objects.get(name=contact_dict['entrust_unit']).addr
dev_addr = Contact.objects.get(name=contact_dict['dev_unit']).addr
test_addr = Contact.objects.get(name=contact_dict['test_unit']).addr
contact_dict['entrust_addr'] = entrust_addr
contact_dict['dev_addr'] = dev_addr
contact_dict['test_addr'] = test_addr
context = {
'datas': contact_dict
}
return create_dg_docx('联系人和方式.docx', context, id)
# 生成测评时间和地点
@route.get('/create/timeaddress', url_name='create-timeaddress')
@transaction.atomic
def create_timeaddress(self, id: int):
doc_timer = DocTime(id)
context = doc_timer.dg_address_time()
return create_dg_docx('测评时间和地点.docx', context, id)
# 生成【主要功能和性能指标】文档片段
@route.get('/create/indicators', url_name='create-indicators')
@transaction.atomic
def create_indicators(self, id: int):
# 获取文档片段模版路径
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '主要功能和性能指标.docx'
doc = DocxTemplate(input_path)
# 获取项目对象
project_obj: Project = get_object_or_404(Project, id=id)
# 定义JINJA上下文
# 获取第一轮次所有功能、性能的设计需求[目前只支持XQ和YZ]因为要获取dut信息进行连表查询
q_ex = Q(dut__type='XQ') | Q(dut__type='YZ')
design_qs = project_obj.psField.filter(q_ex, round__key='0').select_related('dut')
# 1.功能性能覆盖表
# 定义功能/性能两个列表
func_design_list = []
performance_design_list = []
# 遍历设计需求,然后放入两个列表
for design_obj in design_qs:
# 功能指标描述
description = RichParser(design_obj.description).get_final_p_list()
# 覆盖情况-【查询测试项-测试子项名称】
demand_qs = design_obj.dtField.all()
str_list = []
for demand in demand_qs:
# 再查询测试子项
for subDemand in demand.testQField.all():
str_list.append(subDemand.subName)
coverage_str = "".join(str_list)
design_context_obj = {
'chapter_info': f"{design_obj.dut.name}{design_obj.chapter}-{design_obj.name}",
'indicator': "\a".join(description),
'coverage': f"{design_obj.name}进行全覆盖测试,包含{coverage_str},验证所描述内容是否满足需求等文档的要求"
}
demandType_str = get_str_dict(design_obj.demandType, 'demandType')
# 判断是否包含“功能”/“性能”字样
if '功能' in demandType_str:
func_design_list.append(design_context_obj)
elif '性能' in demandType_str:
performance_design_list.append(design_context_obj)
# 2.摸底指标清单
# 先查第一轮次所有测试项
is_has_modi = False
md_demand_list = []
round1_demand_qs = project_obj.ptField.filter(round__key='0')
for one_demand in round1_demand_qs:
testType_str = get_str_dict(one_demand.testType, 'testType')
if '摸底' in testType_str:
is_has_modi = True
md_demand_list.append({
'xq_source': "隐含需求",
'desc': one_demand.testDesciption,
'demand_name': one_demand.name,
'demand_ident': "".join(["XQ_MD_", one_demand.ident])
})
# 上下文添加
context = {
'project_name': project_obj.name,
'func_design_list': func_design_list,
'performance_design_list': performance_design_list,
'md_demand_list': md_demand_list,
'is_has_modi': is_has_modi
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / '主要功能和性能指标.docx')
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 生成测评对象 - 包括大纲、说明、回归说明和报告
@route.get('/create/softComposition', url_name='create-softComposition')
@transaction.atomic
def create_softComposition(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '测评对象.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '测评对象')
context = {
"replace": replace, # 指定是否由数据库文档片段进行生成
"user_content": frag and rich_text_list
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / '测评对象.docx')
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 生成被测软件接口章节
@route.get('/create/interface', url_name='create-interface')
def create_interface(self, id: int):
project_qs = get_object_or_404(Project, id=id)
project_name = project_qs.name
interfaceNameList = []
# 查询接口列表
iters = project_qs.psField.filter(demandType=3)
iters_length = len(iters)
index = 0
for inter in iters:
interfaceNameList.append(inter.name)
index += 1
if index < iters_length:
interfaceNameList.append('')
# 对每个接口进行字典处理
interface_list = []
for interface in iters:
interface_dict = {
'name': interface.name,
'ident': interface.ident,
'source': interface.source,
'to': interface.to,
'type': interface.type,
'protocal': interface.protocal,
}
interface_list.append(interface_dict)
context = {
'project_name': project_name,
'iters': interfaceNameList,
'iter_list': interface_list,
}
return create_dg_docx('被测软件接口.docx', context, id)
# 生成顶层技术文件
@route.get('/create/top_file', url_name='create-performance')
def create_top_file(self, id: int):
project_obj: Project = get_object_or_404(Project, id=id)
is_JD = True if project_obj.report_type == '9' else False
dut_qs = project_obj.pdField.filter(type='YZ')
dut_list = [{
'index': index + 2 if is_JD else index + 1,
'name': dut_obj.name,
'ident_and_version': '-'.join([dut_obj.ref, dut_obj.version]),
'publish_date': dut_obj.release_date,
'source': dut_obj.release_union,
} for index, dut_obj in enumerate(dut_qs)]
context = {
'project_name': project_obj.name,
'is_JD': is_JD,
'dut_list': dut_list,
}
return create_dg_docx('顶层技术文件.docx', context, id)
# 静态测试环境说明
@route.get('/create/static_env', url_name='create-static_env')
def create_static_env(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '静态测试环境说明.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '静态测试环境说明')
context = {
"replace": replace, # 指定是否由数据库文档片段进行生成
"user_content": frag and rich_text_list
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / '静态测试环境说明.docx')
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 静态软件项
@route.get('/create/static_soft', url_name='create-static_soft')
def create_static_soft(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '静态软件项.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '静态软件项')
context = {
"replace": replace, # 指定是否由数据库文档片段进行生成
"user_content": frag and rich_text_list
}
return create_dg_docx("静态软件项.docx", context, id)
# 静态硬件和固件项
@route.get('/create/static_hard', url_name='create-static_hard')
def create_static_hard(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '静态硬件和固件项.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '静态硬件和固件项')
context = {
"replace": replace, # 指定是否由数据库文档片段进行生成
"user_content": frag and rich_text_list
}
return create_dg_docx("静态硬件和固件项.docx", context, id)
# 动态测评环境说明
@route.get('/create/dynamic_env', url_name='create-dynamic_env')
def create_dynamic_env(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '动态测试环境说明.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '动态测试环境说明')
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / '动态测试环境说明.docx')
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 动态软件项
@route.get('/create/dynamic_soft', url_name='create-dynamic_soft')
def create_dynamic_soft(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '动态软件项.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '动态软件项')
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
return create_dg_docx("动态软件项.docx", context, id)
# 动态软件项
@route.get('/create/dynamic_hard', url_name='create-dynamic_hard')
def create_dynamic_hard(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '动态硬件和固件项.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '动态硬件和固件项')
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
return create_dg_docx("动态硬件和固件项.docx", context, id)
# 测试数据
@route.get('/create/test_data', url_name='create-test_data')
def create_test_data(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '测评数据.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '测评数据')
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
return create_dg_docx("测评数据.docx", context, id)
# 环境差异性分析
@route.get('/create/env_diff', url_name='create-env_diff')
def create_env_diff(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '环境差异性分析.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '环境差异性分析')
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
return create_dg_docx("环境差异性分析.docx", context, id)
# 生成被测软件-基本信息
@route.get('/create/baseInformation', url_name='create-baseInformation')
def create_information(self, id: int):
project_qs = get_object_or_404(Project, id=id)
security = get_str_dict(project_qs.security_level, 'security_level')
languages = get_list_dict('language', project_qs.language)
runtime = get_str_dict(project_qs.runtime, 'runtime')
devplant = get_str_dict(project_qs.devplant, 'devplant')
language_list = []
for language in languages:
language_list.append(language.get('ident_version'))
# 版本先找第一轮
project_round = project_qs.pField.filter(key=0).first()
first_round_SO = project_round.rdField.filter(type='SO').first()
if not first_round_SO:
return ChenResponse(code=400, status=400, message='您还未创建轮次,请进入工作区创建')
version = first_round_SO.version
line_count = int(first_round_SO.total_lines)
dev_unit = project_qs.dev_unit
# 渲染上下文
context = {
'project_name': project_qs.name,
'security_level': security,
'language': "\a".join(language_list),
'version': version,
'line_count': line_count,
'effective_line': int(first_round_SO.effective_lines),
'recv_date': project_qs.beginTime.strftime("%Y-%m-%d"),
'dev_unit': dev_unit,
'soft_type': project_qs.get_soft_type_display(),
'runtime': runtime,
'devplant': devplant
}
return create_dg_docx('被测软件基本信息.docx', context, id)
# 生成测试级别和测试类型
@route.get('/create/levelAndType', url_name='create-levelAndType')
def create_levelAndType(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '测试级别和测试类型.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '测试级别和测试类型')
if replace:
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
else:
# 如果没有片段替换,则利用数据生成信息
project_qs = get_object_or_404(Project, id=id)
# 获取所有已录入测试类型
test_types = project_qs.ptField.values("testType").distinct()
# 通过测试类型查询字典中的中文
type_name_list = list(map(lambda qs_item: get_str_dict(qs_item['testType'], 'testType'), test_types))
# 定义测试类型一览的顺序注意word里面也要一样
word_types = ['文档审查', '静态分析', '代码审查', '逻辑测试', '功能测试', '性能测试', '边界测试',
'恢复性测试', '安装性测试', '数据处理测试', '余量测试', '强度测试', '接口测试',
'人机交互界面测试', '兼容性测试']
type_index = []
for index, test_type in enumerate(word_types):
for exist_type in type_name_list:
if exist_type == test_type:
type_index.append(str(index))
context = {
"testTypes": "".join(type_name_list),
"project_name": project_qs.name,
"type_index": type_index
}
return create_dg_docx("测试级别和测试类型.docx", context, id)
# 生成测试策略
@route.get('/create/strategy', url_name='create-strategy')
def create_strategy(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '测试策略.docx'
doc = DocxTemplate(input_path)
replace, frag, rich_text_list = self._generate_frag(id, doc, '测试策略')
if replace:
context = {
"replace": replace,
"user_content": frag and rich_text_list
}
else:
# 如果没有片段替换,则利用数据生成信息
project_qs = get_object_or_404(Project, id=id)
# 根据关键等级检查是否有代码审查
security = project_qs.security_level
isDmsc = True if int(security) <= 2 else False
# 获取当前测试项的测试类型
test_types = project_qs.ptField.values("testType").distinct()
type_name_list = list(map(lambda qs_item: get_str_dict(qs_item['testType'], 'testType'), test_types))
context = {
"project_name": project_qs.name,
# 查询关键等级-类似“关键”输出
"security_level_str": get_str_abbr(security, 'security_level'),
"isDmsc": isDmsc,
"test_types": type_name_list
}
return create_dg_docx("测试策略.docx", context, id)
# 生成-测试内容充分性及测试方法有效性
@route.get('/create/adequacy_effectiveness', url_name='create-adequacy_effectiveness')
def create_adequacy_effectiveness(self, id: int):
project_qs = get_object_or_404(Project, id=id)
# 统计测试种类数量-只统计第一轮测试
project_round_one = project_qs.pField.filter(key=0).first()
if not project_round_one:
return ChenResponse(status=400, code=400, message="未找到首轮测试信息!")
# 通过字典获取-测试方法
type_dict = {} # key为测试类型value为数量
testDemands = project_round_one.rtField.all()
for testDemand in testDemands:
# 获取每个测试项测试类型
test_type = get_list_dict('testType', [testDemand.testType])[0].get('ident_version')
# 如果字典没有该key则创建并value=1
if not test_type in type_dict:
type_dict[test_type] = 1
else:
type_dict[test_type] += 1
length = len(type_dict)
type_str_list = []
for key, value in type_dict.items():
type_str_list.append(f"{key}{value}")
context = {
'project_name': project_qs.name,
'length': length,
'type_str': "".join(type_str_list),
}
return create_dg_docx('测试内容充分性及测试方法有效性分析.docx', context, id)
# 生成-测评项目组组成和分工
@route.get('/create/group', url_name='create_group')
def create_group(self, id: int):
project_qs = get_object_or_404(Project, id=id)
context = {
'duty_person': project_qs.duty_person,
'member_str': "".join(project_qs.member),
'quality_person': project_qs.quality_person,
'vise_person': project_qs.vise_person,
'config_person': project_qs.config_person,
'dev_unit': project_qs.dev_unit,
}
return create_dg_docx('测评组织及任务分工.docx', context, id)
# 生成-测评条件保障
@route.get('/create/guarantee', url_name='create-guarantee')
def create_guarantee(self, id: int):
project_qs = get_object_or_404(Project, id=id)
context = {
'project': project_qs
}
return create_dg_docx('测评条件保障.docx', context, id)
# 生成-缩略语
@route.get('/create/abbreviation', url_name='create-abbreviation')
def create_abbreviation(self, id: int):
project_qs = get_object_or_404(Project, id=id)
abbreviations = []
for abbr in project_qs.abbreviation:
abbr_dict = {'title': abbr, 'des': Abbreviation.objects.filter(title=abbr).first().des}
abbreviations.append(abbr_dict)
context = {
'abbreviations': abbreviations
}
return create_dg_docx('缩略语.docx', context, id)
# 生成研制总要求-测试项追踪关系表
@route.get('/create/yzComparison', url_name='create-yzComparison')
def create_yzComparison(self, id: int):
"""目前追踪需求项的章节号是硬编码按6.2章节起步6.2.1~x.x.x依次排序"""
# 规定测试项的章节号开头
test_item_prefix = '6.2'
# 计算有多少种testType - '文档审查'/'功能测试' ->
# 形成一个数组['1','2','3','4','9']后面用来判断测试项的章节号
project_qs = get_object_or_404(Project, id=id)
design_list = [] # 先按照design的思路进行追踪
# 判断是否为鉴定测评,有则生成该表
if project_qs.report_type == '9':
project_round_one = project_qs.pField.filter(key=0).first()
testType_list, last_chapter_items = create_csx_chapter_dict(project_round_one)
# 找出第一轮的研总
yz_dut = project_round_one.rdField.filter(type='YZ').first()
if yz_dut:
# 查询出验证所有design
yz_designs = yz_dut.rsField.all()
# 遍历所有研总的design
for design in yz_designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
# 获取一个design的所有测试项
test_items = design.dtField.all()
for test_item in test_items:
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([test_item_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident}
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
context = {
'design_list': design_list
}
return create_dg_docx('研制总要求追踪表.docx', context, id)
# 生成需求规格说明-测试项追踪关系表
@route.get('/create/xqComparison', url_name='create-xqComparison')
def create_xqComparison(self, id: int):
project_qs = get_object_or_404(Project, id=id)
test_item_prefix = '6.2'
design_list = []
project_round_one = project_qs.pField.filter(key=0).first()
if project_round_one:
testType_list, last_chapter_items = create_csx_chapter_dict(project_round_one)
# 找出第一轮的被测件为'XQ'
xq_dut = project_round_one.rdField.filter(type='XQ').first()
# 找出第一轮被测件为'SO',其中的测试项
so_dut = project_round_one.rdField.filter(type='SO').first()
if so_dut:
so_designs = so_dut.rsField.all()
for design in so_designs:
design_dict = {'name': "/", 'chapter': "/", 'test_demand': []}
# 获取一个design的所有测试项
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
# 只对文档审查、静态分析、代码走查、代码审查进行处理
if test_item.testType in ['8', '15', '3', '2']:
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([test_item_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident}
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
if xq_dut:
xq_designs = xq_dut.rsField.all()
for design in xq_designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
# 获取一个design的所有测试项
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([test_item_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident}
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
context = {
'design_list': design_list
}
return create_dg_docx('需求规格说明追踪表.docx', context, id)
raise HttpError(400, "生成需求追踪表出错")
# 生成测试项-需求规格说明关系表【反向】
@route.get('/create/fanXqComparison', url_name='create-fanXqComparison')
def create_fanXqComparison(self, id: int):
project_qs = get_object_or_404(Project, id=id)
test_item_prefix = '6.2'
# 取出第一轮所有测试项的章节处理列表和字典
project_round_one = project_qs.pField.filter(key=0).first()
testType_list, last_chapter_items = create_csx_chapter_dict(project_round_one)
# 查询第一轮所有测试项
test_items = []
test_items.extend(project_round_one.rtField.all())
# 最后渲染列表
items_list = []
for test_item in test_items:
# 第二个处理被测件为"XQ",第二个处理被测件为'SO'并且为测试项testType为['8', '15', '3', '2']的
if test_item.dut.type == 'XQ' or (test_item.dut.type == 'SO' and test_item.testType in ['8', '15', '3',
'2']):
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([test_item_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
# 如果是SO里面的
if test_item.testType in ['8', '15', '3', '2'] and test_item.dut.type == 'SO':
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'design': {
'name': "/", 'chapter': "/"
}}
else:
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'design': {
'name': test_item.design.name, 'chapter': test_item.design.chapter
}}
items_list.append(test_item_dict)
context = {
'items_list': items_list,
}
return create_dg_docx('反向需求规格追踪表.docx', context, id)
# 生成代码质量度量分析表
@route.get('/create/codeQuality', url_name='create-codeQuality')
def create_codeQuality(self, id: int):
project_qs = get_object_or_404(Project, id=id)
project_round_one = project_qs.pField.filter(key=0).first()
context = {}
context.update({'project_name': project_qs.name})
if project_round_one:
source_dut: Dut = project_round_one.rdField.filter(type='SO').first() # type:ignore
if source_dut:
context.update({'version': source_dut.version}) # type:ignore
context.update({'size': int(source_dut.total_lines)})
context.update({'total_code_line': int(source_dut.effective_lines)})
context.update({'comment_line': int(source_dut.comment_lines)})
comment_ratio = int(source_dut.comment_lines) / int(source_dut.total_lines)
context.update({
'comment_ratio': f"{comment_ratio * 100:.2f}%",
'comment_ratio_right': '满足' if comment_ratio >= 0.2 else '不满足'
})
# 如果已经有metrics
if hasattr(source_dut, 'metrics'):
context.update({
'black_line': source_dut.metrics.total_blanks,
'function_count': source_dut.metrics.function_count,
'avg_function_lines': source_dut.metrics.avg_function_lines,
'avg_function_lines_right': '满足' if source_dut.metrics.avg_function_lines <= 200 else '不满足',
'avg_fan_out': source_dut.metrics.avg_fan_out,
'avg_fan_out_right': '满足' if source_dut.metrics.avg_fan_out <= 7 else '不满足',
'avg_cyclomatic': source_dut.metrics.avg_cyclomatic,
'avg_cyclomatic_right': '满足' if source_dut.metrics.avg_cyclomatic <= 10 else '不满足',
'max_cyclomatic': source_dut.metrics.max_cyclomatic,
'max_cyclomatic_right': '满足' if source_dut.metrics.max_cyclomatic <= 80 else '不满足',
'high_cyclomatic_ratio': source_dut.metrics.high_cyclomatic_ratio,
'high_cyclomatic_ratio_right': '满足' if source_dut.metrics.high_cyclomatic_ratio <= 0.2 else '不满足',
})
else:
return ChenResponse(message='未找到源代码被测件', code=400)
return create_dg_docx('代码质量度量分析表.docx', context, id)

View File

@@ -0,0 +1,212 @@
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='多轮回归测试用例记录生成完毕')

View File

@@ -0,0 +1,602 @@
import base64
import io
from pathlib import Path
from copy import deepcopy
from typing import Union
from ninja_extra import api_controller, ControllerBase, route
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
from ninja.errors import HttpError
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.db.models import QuerySet, Q
from docxtpl import DocxTemplate, RichText, InlineImage
from docx.shared import Mm
from docx import Document
# 导入模型
from apps.project.models import Project, Round, Dut
from apps.dict.models import Dict, DictItem
# 导入项目工具
from utils.util import get_list_dict, get_str_dict, MyHTMLParser, get_ident, get_case_ident, get_testType
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from utils.chen_response import ChenResponse
from apps.createDocument.extensions import util
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.createDocument.extensions.documentTime import DocTime
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
chinese_round_name: list = ['', '', '', '', '', '', '', '', '', '']
# @api_controller("/generateHSM", tags=['生成回归说明系列文档'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generateHSM", tags=['生成回归说明系列文档'])
class GenerateControllerHSM(ControllerBase):
logger = GenerateLogger('回归测试说明')
# important删除之前的文件
@route.get('/create/deleteHSMDocument', url_name='delete-hsm-document')
def delete_hsm_document(self, id: int):
project_path_str = project_path(id)
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hsm'
try:
delete_dir_files(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='另一个程序正在占用文件,请关闭后重试')
@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/hsm' / '被测软件基本信息.docx'
doc = DocxTemplate(tpl_path)
project_obj: Project = 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': round1_so_dut.total_lines,
'effective_count': 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': so_dut.total_lines,
'effective_count': so_dut.effective_lines,
}
)
context_round['version_info'] = version_info
# 开始渲染每个轮次的二级文档
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hsm' / 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/docsummary", url_name="create-docsummary")
@transaction.atomic
def create_docsummary(self, id: int):
"""生成回归测试说明的文档概述"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hsm' / '文档概述.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
context = {
'project_obj': project_obj.name,
}
for hround in hround_list:
# 取出当前轮次key减1就是上一轮次
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}轮次中缺少源代码被测件,请添加')
# 取上一轮次
so_dut_last: Dut = Dut.objects.filter(round__key=str(int(hround.key) - 1), project=project_obj,
type='SO').first()
round_context = deepcopy(context)
round_context['current_version'] = so_dut.version
round_context['last_version'] = so_dut_last.version
round_context['round_chinese'] = cname
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hsm' / f"{cname}轮文档概述.docx"
doc.render(context=round_context)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归文档概述生成完毕')
@route.get("/create/jstech", url_name="create-jstech")
@transaction.atomic
def create_jstech(self, id: int):
"""生成回归测试说明的技术依据文件"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hsm' / '技术依据文件.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
duties_qs = project_obj.pdField.filter(Q(type='XQ') | Q(type='SJ') | Q(type='XY'))
std_documents = []
for duty in duties_qs:
one_duty = {'doc_name': duty.name, 'ident_version': duty.ref + '-' + duty.version,
'publish_date': duty.release_date, 'source': duty.release_union}
std_documents.append(one_duty)
doc_name = f'{project_obj.name}软件测评大纲'
if project_obj.report_type == '9':
doc_name = f'{project_obj.name}软件鉴定测评大纲'
# 时间控制类
timer = DocTime(id)
dg_duty = {'doc_name': doc_name, 'ident_version': f'PT-{project_obj.ident}-TO-1.00',
'publish_date': timer.dg_cover_time, 'source': project_obj.test_unit}
std_documents.append(dg_duty)
# 需要添加说明、记录
sm_duty = {'doc_name': f'{project_obj.name}软件测试说明', 'ident_version': f'PT-{project_obj.ident}-TD-1.00',
'publish_date': timer.sm_cover_time, 'source': project_obj.test_unit}
jl_duty = {'doc_name': f'{project_obj.name}软件测试记录', 'ident_version': f'PT-{project_obj.ident}-TN',
'publish_date': timer.jl_cover_time, 'source': project_obj.test_unit}
std_documents.extend([sm_duty, jl_duty])
# 非第一轮的轮次
hround_list: QuerySet = project_obj.pField.exclude(key='0')
if len(hround_list) < 1:
return None
for hround in hround_list:
std_documents_round = deepcopy(std_documents)
# 取出当前轮次key
cname = chinese_round_name[int(hround.key)]
hsm_duty = {'doc_name': f'{project_obj.name}软件第{cname}轮测试说明',
'ident_version': f'PT-{project_obj.ident}-TD{int(hround.key) + 1}-1.00',
'publish_date': hround.beginTime, 'source': project_obj.test_unit}
hjl_duty = {'doc_name': f'{project_obj.name}软件第{cname}轮测试记录',
'ident_version': f'PT-{project_obj.ident}-TN{int(hround.key) + 1}',
'publish_date': hround.endTime, 'source': project_obj.test_unit}
std_documents.extend([hsm_duty, hjl_duty])
context = {
'std_documents': std_documents_round
}
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hsm' / f"{cname}轮技术依据文件.docx"
doc.render(context=context)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归技术依据文件生成完毕')
@route.get("/create/changePart", url_name="create-changePart")
@transaction.atomic
def create_changePart(self, id: int):
"""
生成回归测试说明的软件更改部分
暂时没想到如何处理和报告里面软件更改部分关系
"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hsm' / '软件更改部分.docx'
doc = DocxTemplate(tpl_path)
project_obj = get_object_or_404(Project, id=id)
context = {
'project_name': project_obj.name,
}
# 非第一轮的轮次
hround_list: QuerySet = project_obj.pField.exclude(key='0')
if len(hround_list) < 1:
return None
for hround in hround_list:
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}轮次中缺少源代码被测件,请添加')
xq_dut: Dut = hround.rdField.filter(type='XQ').first()
# 处理代码版本
last_round_key = str(int(hround.key) - 1)
last_round: Round = project_obj.pField.filter(key=last_round_key).first()
last_round_so_dut = last_round.rdField.filter(type='SO').first()
if not last_round_so_dut:
return ChenResponse(code=400, status=400,
message=f'您第{chinese_round_name[int(hround.key)]}轮次中缺少源代码版本信息,请添加')
last_dm_version = last_round_so_dut.version
now_dm_version = so_dut.version
# 如果存在这个轮次的需求文档,则查询上个版本
last_xq_version = ""
if xq_dut:
last_xq_dut = last_round.rdField.filter(type='XQ').first()
if not last_xq_dut:
return ChenResponse(code=400, status=400,
message=f'您第{chinese_round_name[int(hround.key)]}轮次中缺少需求文档信息')
last_xq_version = last_xq_dut.version
# 如果当前轮次有需求文档的修改
now_xq_version = xq_dut.version
context_round['xq_str'] = f",以及软件需求规格说明{now_xq_version}版本和{last_xq_version}版本"
else:
# 如果当前轮次没有需求文档则xq_str为空
context_round['xq_str'] = ""
context_round['so_str'] = f"被测软件代码{now_dm_version}版本和{last_dm_version}版本"
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hsm' / f"{cname}轮软件更改部分.docx"
doc.render(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/hdemand", url_name="create-hdemand")
@transaction.atomic
def create_hdemand(self, id: int):
"""
生成非第一轮的多个测试需求
"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hsm' / '回归测试需求.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
# 遍历非第一轮的轮次
for hround in hround_list:
cname = chinese_round_name[int(hround.key)] # var输出二、三字样
# 先查询dict字典查出总共有多少个testType
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)]
# 获得本轮次所有testDemand
testDemand_qs = hround.rtField.all()
for demand in testDemand_qs:
type_index = type_number_list.index(int(demand.testType))
content_list = []
for (index, content) in enumerate(demand.testQField.all()):
content_dict = {
"index": index + 1,
"rindex": str(index + 1).rjust(2, '0'),
"subName": content.subName,
# 修改遍历content下面的stepcontent变量是TestDemandContent表
"subStep": [
{'index': index + 1, 'operation': step_obj.operation, 'expect': step_obj.expect}
for (index, step_obj) in enumerate(content.testStepField.all())
],
}
content_list.append(content_dict)
testmethod_str = ''
for dict_item_qs in Dict.objects.get(code="testMethod").dictItem.all():
for tm_item in demand.testMethod:
if tm_item == dict_item_qs.key:
testmethod_str += dict_item_qs.title + " "
# 设计需求的描述,富文本
parser = RichParser(demand.design.description)
# 查询关联design以及普通design
doc_list = [{'dut_name': demand.dut.name, 'design_chapter': demand.design.chapter,
'design_name': demand.design.name}]
for relate_design in demand.otherDesign.all():
ddict = {'dut_name': relate_design.dut.name, 'design_chapter': relate_design.chapter,
'design_name': relate_design.name}
doc_list.append(ddict)
# 组装单个测试项
testdemand_dict = {
"name": demand.name,
"key": demand.key,
"ident": get_ident(demand),
"priority": get_str_dict(demand.priority, "priority"),
"doc_list": doc_list,
"design_description": parser.get_final_list(doc),
"test_demand_content": content_list,
"testMethod": testmethod_str,
"adequacy": demand.adequacy.replace("\n", "\a"),
"testDesciption": demand.testDesciption.replace("\n", "\a") # 测试项描述
}
list_list[type_index].append(testdemand_dict)
# 定义渲染context字典
context = {
"project_name": project_obj.name
}
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/hsm' / 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='多轮回归测试需求生成完毕')
@route.get("/create/caseListDesc", url_name="create-caseListDesc")
@transaction.atomic
def create_caseListDesc(self, id: int):
"""
生成非第一轮的用例说明
"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hsm' / '回归测试用例概述.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
for hround in hround_list:
# 先查询dict字典查出总共有多少个testType
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)]
cname = chinese_round_name[int(hround.key)] # 输出二、三...
testDemands = hround.rtField.all()
for demand in testDemands:
type_index = type_number_list.index(int(demand.testType))
demand_ident = get_ident(demand)
demand_dict = {
'name': demand.name,
'item': []
}
for case in demand.tcField.all():
case_dict = {
'name': case.name,
'ident': get_case_ident(demand_ident, case),
'summary': case.summarize,
}
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))
sort = qs.sort
table = {
"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/hsm' / f"{cname}轮回归测试用例概述.docx"
doc.render(context=context)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮回归测试用例概述生成完毕')
@route.get("/create/caseList", url_name="create-caseList")
@transaction.atomic
def create_caseList(self, id: int):
"""
生成非第一轮的测试用例
"""
project_path_str = project_path(id)
tpl_path = Path.cwd() / 'media' / project_path_str / 'form_template/hsm' / '测试用例.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
for hround in hround_list:
cname = chinese_round_name[int(hround.key)] # 输出二、三...
# 先查询dict字典查出总共有多少个testType
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)]
demand_prefix = '3.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=70)
step_dict = {
'index': index,
'operation': desc_list,
'expect': one.expect,
}
step_list.append(step_dict)
index += 1
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,
'step': step_list
}
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
context["round_han"] = cname
save_path = Path.cwd() / 'media' / project_path_str / 'output_dir/hsm' / f"{cname}轮测试用例.docx"
doc.render(context=context)
try:
doc.save(save_path)
except PermissionError:
return ChenResponse(code=400, status=400, message='您打开了生成的文档,请关闭后重试')
return ChenResponse(code=200, status=200, message='多轮测试用例生成完毕')
@route.get("/create/track", url_name="create-track")
@transaction.atomic
def create_track(self, id: int):
"""
生成非第一轮的用例追踪
"""
project_path_str = project_path(id)
project_obj = get_object_or_404(Project, id=id)
# 非第一轮轮次对象
hround_list: QuerySet = project_obj.pField.exclude(key='0')
demand_prefix = '4.1'
if len(hround_list) < 1:
return
for hround in hround_list:
# 取出当前轮次key减1就是上一轮次
cname = chinese_round_name[int(hround.key)] # 输出二、三...
design_list = []
testType_list, last_chapter_items = create_csx_chapter_dict(hround)
# 找出当前轮次被测件为'SO'的
so_dut = hround.rdField.filter(type='SO').first()
if not so_dut:
return ChenResponse(code=400, status=400, message=f'{cname}轮次无源代码被测件')
so_designs = so_dut.rsField.filter()
for design in so_designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
if test_item.testType in ['2', '3', '15', '8']:
design_dict.update({'name': "/", 'chapter': "/"})
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([demand_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'case_list': []}
for case in test_item.tcField.all():
case_dict = {
'name': case.name,
'ident': get_case_ident(reveal_ident, case)
}
test_item_dict['case_list'].append(case_dict)
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
# 找出当前轮次的被测件为'XQ'的第一个
xq_dut = hround.rdField.filter(type='XQ').first()
if not xq_dut:
return ChenResponse(code=400, status=400,
message=f'{cname}轮次没有找到需求被测件,只有放在被测件为<需求>的设计需求、测试项、用例才会被追踪')
xq_designs = xq_dut.rsField.all()
for design in xq_designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([demand_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'case_list': []}
for case in test_item.tcField.all():
case_dict = {
'name': case.name,
'ident': get_case_ident(reveal_ident, case)
}
test_item_dict['case_list'].append(case_dict)
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
context = {
'design_list': design_list,
}
# 手动渲染tpl生成文档
input_file = Path.cwd() / 'media' / project_path_str / 'form_template' / 'hsm' / '用例追踪.docx'
temporary_file = Path.cwd() / 'media' / project_path_str / 'form_template' / 'hsm' / 'temporary' / f'{cname}轮用例追踪_temp.docx'
out_put_file = Path.cwd() / 'media' / project_path_str / 'output_dir' / 'hsm' / f'{cname}轮用例追踪.docx'
doc = DocxTemplate(input_file)
doc.render(context)
doc.save(temporary_file)
# 通过docx合并单元格
if temporary_file.is_file():
try:
docu = Document(temporary_file)
# 找到其中的表格
util.merge_all_cell(docu.tables[0])
# 储存到合适位置
docu.save(out_put_file)
except PermissionError:
return ChenResponse(code=400, status=400, message='请检查文件是否打开,如果打开则关闭...')
else:
return ChenResponse(code=400, status=400, message='中间文档未找到,请检查你模版是否存在...')
return ChenResponse(code=200, status=200, message='文档生成成功...')

View File

@@ -0,0 +1,154 @@
# 内置模块导入
from pathlib import Path
# 导入框架相关
from django.db import transaction
from django.shortcuts import get_object_or_404
from ninja_extra import api_controller, ControllerBase, route
# 导入权限相关
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
# 导入数据库模型
from apps.project.models import Project
from apps.dict.models import Dict
from docxtpl import InlineImage
# 导入文档处理相关
from docxtpl import DocxTemplate
# 导入自己工具
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from utils.util import get_ident, get_case_ident, get_testType
from utils.chen_response import ChenResponse
from utils.path_utils import project_path
from apps.createDocument.extensions.parse_rich_text import RichParser
# @api_controller("/generateJL", tags=['生成测试记录系列'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generateJL", tags=['生成测试记录系列'])
class GenerateControllerJL(ControllerBase):
@route.get("/create/caserecord", url_name="create-caserecord")
@transaction.atomic
def create_caserecord(self, id: int):
"""生成测试记录表格"""
project_path_str = project_path(id)
project_obj = get_object_or_404(Project, id=id)
# 测试用例记录模版位置
record_template_path = Path.cwd() / 'media' / project_path_str / 'form_template/jl' / '测试用例记录.docx'
# 打开模版文档
doc = DocxTemplate(record_template_path)
# 准备返回的测试类型数组的嵌套
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)] # 每个测试类型组合为一个列表[[],[],[],[]]
# 查询出第一轮次
round_one = project_obj.pField.filter(key=0).first()
# 测试项的章节号预置处理 - 根据轮次生成list和dict的二维数据
demand_prefix = '6.2'
testType_list, last_chapter_items = create_csx_chapter_dict(round_one)
# 找出所有测试项
testDemands = round_one.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)
# 这里需要对result富文本处理
rich_parser2 = RichParser(one.result)
res_list = rich_parser2.get_final_list(doc, img_size=75)
# 组装用例里面的步骤dict
passed = '通过'
if one.passed == '2':
passed = '未通过'
elif one.passed == '3':
passed = '未执行'
step_dict = {
'index': index,
'operation': desc_list,
'expect': one.expect,
'result': res_list,
'passed': passed,
}
index += 1
step_list.append(step_dict)
# 这里判断里面的单个步骤的执行情况,来输出一个整个用例的执行情况
exe_noncount = 0
execution_str = '已执行'
for ste in step_list:
if ste.get('execution') == '3':
exe_noncount += 1
if exe_noncount > 0 and exe_noncount != len(step_list):
execution_str = '部分执行'
elif exe_noncount == len(step_list):
execution_str = '未执行'
else:
execution_str = '已执行'
# 查询所有的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]))
# 组装用例的dict
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
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,
'execution': execution_str,
'time': str(case.exe_time) if case.exe_time is not None else str(case.update_datetime),
'problems': "".join(problem_list),
# 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)) # type:ignore
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
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path_str / "output_dir/jl" / "测试用例记录.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))

View File

@@ -0,0 +1,282 @@
from pathlib import Path
# 导入框架东西
from ninja_extra import ControllerBase, api_controller, route
# 框架组件
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.db.models import Q
# 认证和权限
from ninja_extra.permissions import IsAuthenticated
from ninja_jwt.authentication import JWTAuth
# 导入模型
from apps.project.models import Project
from apps.dict.models import Dict
# 导入文档处理类
from docxtpl import DocxTemplate
from docx import Document
# 导入自己工具
from utils.chen_response import ChenResponse
from utils.util import get_ident, get_case_ident, get_testType
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from apps.createDocument.extensions import util
from apps.createDocument.extensions.util import create_sm_docx
from utils.path_utils import project_path
from apps.createDocument.extensions.parse_rich_text import RichParser
from apps.createDocument.extensions.documentTime import DocTime
# @api_controller("/generateSM", tags=['生成说明文档系列'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generateSM", tags=['生成说明文档系列'])
class GenerateControllerSM(ControllerBase):
@route.get("/create/techyiju", url_name="create-techyiju")
@transaction.atomic
def create_techyiju(self, id: int):
project_obj = get_object_or_404(Project, id=id)
duties_qs = project_obj.pdField.filter(Q(type='XQ') | Q(type='SJ') | Q(type='XY'))
std_documents = []
for duty in duties_qs:
one_duty = {'doc_name': duty.name, 'ident_version': duty.ref + '-' + duty.version,
'publish_date': duty.release_date, 'source': duty.release_union}
std_documents.append(one_duty)
# 添加大纲到这里
## 判断是否为鉴定
doc_name = f'{project_obj.name}软件测评大纲'
if project_obj.report_type == '9':
doc_name = f'{project_obj.name}软件鉴定测评大纲'
# 这里大纲版本升级如何处理 - TODO1.大纲版本升级后版本处理
# 时间控制类
timer = DocTime(id)
dg_duty = {'doc_name': doc_name, 'ident_version': f'PT-{project_obj.ident}-TO-1.00',
'publish_date': timer.dg_cover_time, 'source': project_obj.test_unit}
std_documents.append(dg_duty)
# 生成二级文档
context = {
'std_documents': std_documents
}
return create_sm_docx("技术依据文件.docx", context, id)
@route.get('/create/caseList', url_name='create-caseList')
@transaction.atomic
def create_caseList(self, id: int):
"""创建第一轮文档"""
project_path_str = project_path(id)
# 生成测试用例需要doc对象
case_template_doc_path = Path.cwd() / 'media' / project_path_str / 'form_template/sm' / '测试用例.docx'
doc = DocxTemplate(case_template_doc_path)
project_obj = get_object_or_404(Project, id=id)
# 先查询dict字典查出总共有多少个testType
test_type_len = Dict.objects.get(code='testType').dictItem.count()
type_number_list = [i for i in
range(1, test_type_len + 1)] # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
list_list = [[] for j in
range(1, test_type_len + 1)] # [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
# 先找到第一轮次
project_round_one = project_obj.pField.filter(key=0).first()
# 测试项的章节号预置处理
demand_prefix = '6.2'
testType_list, last_chapter_items = create_csx_chapter_dict(project_round_one)
testDemands = project_round_one.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=70)
step_dict = {
'index': index,
'operation': desc_list,
'expect': one.expect,
}
step_list.append(step_dict)
index += 1
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,
'step': step_list
}
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
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path_str / "output_dir/sm" / "测试用例.docx")
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
@route.get('/create/caseBreifList', url_name='create-caseBreifList')
@transaction.atomic
def create_caseBreifList(self, id: int):
# 生成第一轮的测试说明
project_obj = get_object_or_404(Project, id=id)
# 先查询dict字典查出总共有多少个testType
test_type_len = Dict.objects.get(code='testType').dictItem.count()
type_number_list = [i for i in
range(1, test_type_len + 1)] # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
list_list = [[] for j in
range(1, test_type_len + 1)] # [[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
# 先找到第一轮次
project_round_one = project_obj.pField.filter(key=0).first()
# 找到第一轮的全部测试项
testDemands = project_round_one.rtField.all()
for demand in testDemands:
type_index = type_number_list.index(int(demand.testType))
demand_ident = get_ident(demand)
demand_dict = {
'name': demand.name,
'item': []
}
for case in demand.tcField.all():
case_dict = {
'name': case.name,
'ident': get_case_ident(demand_ident, case),
'summary': case.summarize,
}
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))
sort = qs.sort
table = {
"item": li,
"sort": sort
}
output_list.append(table)
output_list = sorted(output_list, key=(lambda x: x["sort"]))
context["data"] = output_list
return create_sm_docx("用例说明.docx", context, id)
@route.get('/create/smtrack', url_name='create-smtrack')
@transaction.atomic
def create_smtrack(self, id: int):
"""生成说明的需求追踪表"""
project_path_str = project_path(id)
project_obj = get_object_or_404(Project, id=id)
demand_prefix = '6.2'
design_list = []
project_round_one = project_obj.pField.filter(key='0').first()
if project_round_one:
testType_list, last_chapter_items = create_csx_chapter_dict(project_round_one)
# 找出第一轮被测件为'SO'的
so_dut = project_round_one.rdField.filter(type='SO').first()
if so_dut:
so_designs = so_dut.rsField.all()
for design in so_designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
# 对4个测试类型单独处理因为这4类肯定没有章节号
if test_item.testType in ['2', '3', '15', '8']:
design_dict.update({'name': "/", 'chapter': "/"})
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([demand_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'case_list': []}
for case in test_item.tcField.all():
case_dict = {
'name': case.name,
'ident': get_case_ident(reveal_ident, case)
}
test_item_dict['case_list'].append(case_dict)
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
# 上面找出了源代码被测件这里找XQ被测件
xq_dut = project_round_one.rdField.filter(type='XQ').first()
if xq_dut:
xq_designs = xq_dut.rsField.all()
for design in xq_designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
# 获取一个design的所有测试项
# 注意:这里有关联测试项!!!需要多对多关系拼接
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
# 查字典方式确认章节号最后一位
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([demand_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'case_list': []}
for case in test_item.tcField.all():
case_dict = {
'name': case.name,
'ident': get_case_ident(reveal_ident, case)
}
test_item_dict['case_list'].append(case_dict)
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
context = {
'design_list': design_list,
}
# 手动渲染tpl生成文档
input_file = Path.cwd() / 'media' / project_path_str / 'form_template' / 'sm' / '说明追踪.docx'
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.save(temporary_file)
# 通过docx合并单元格
if temporary_file.is_file():
try:
docu = Document(temporary_file)
# 找到其中的表格
util.merge_all_cell(docu.tables[0])
# 储存到合适位置
docu.save(out_put_file)
return ChenResponse(code=200, status=200, message='文档生成成功...')
except PermissionError as e:
return ChenResponse(code=400, status=400, message='请检查文件是否打开,如果打开则关闭...')
else:
return ChenResponse(code=400, status=400, message='中间文档未找到,请检查你模版是否存在...')

View File

@@ -0,0 +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))

View File

@@ -0,0 +1,80 @@
from apps.project.models import Project
from utils.util import *
from utils.chen_response import ChenResponse
from django.db.models import Q
def create_round_context(project_obj: Project, round_id: str):
"""根据轮次,生成测评报告中的测评结果"""
# 0. 首先定义个轮次对应中文
round_chinese = ['', '', '', '', '', '', '', '', '', '']
round_id = int(round_id)
round_str_id = str(round_id)
# 1. 首先先查询给定轮次的round对向
round_obj = project_obj.pField.filter(key=round_str_id).first()
# 如果没有轮次对象则返回错误
if not round_obj:
return ChenResponse(code=400, status=400, message='注意您没有设置第二轮测试,请添加!')
dut_qs_r2 = round_obj.rdField # 当前轮被测件dut对象qs
dut_qs_r1 = project_obj.pdField.filter(round__key=str(round_id - 1)) # 上一个轮次被测件dut对象qs
# 查询当前轮次duts是否有源代码如果没有返回错误
so_dut = dut_qs_r2.filter(type='SO').first() # 当前轮次源代码被测件对象
if not so_dut:
return ChenResponse(code=400, status=400, message='注意您某轮次没有编写源代码被测件信息,务必添加...')
# 查询上一个轮次的dut中的源代码、需求文档
r1_so_dut = dut_qs_r1.filter(type='SO').first()
# 3. 文档审查清单
doc_list = []
round_duts = round_obj.rdField.filter(Q(type='SJ') | Q(type='XQ') | Q(type='XY'))
for dut in round_duts:
dut_dict = {
'name': dut.name,
'ident': dut.ref,
'version': dut.version
}
doc_list.append(dut_dict)
# 4. 发现多少个问题,什么类型多少个问题
problems = project_obj.projField.all().distinct() # !important:大变量-项目所有问题
problems_r2 = problems.filter(case__round__key=round_str_id) # 当前轮次所有问题
# 7. 第二轮动态测试用例个数(动态测试-非静态分析、文档审查、代码审查、代码走查4个)
case_r2_qs = round_obj.rcField.filter(~Q(test__testType='2'), ~Q(test__testType='3'), ~Q(test__testType='8'),
~Q(test__testType='15')) # !warning:中变量-第一轮动态测试用例qs
testType_list, testType_count = create_str_testType_list(case_r2_qs)
## 动态测试(第一轮)各个类型测试用例执行表/各个测试需求表
demand_r2_dynamic_qs = round_obj.rtField.filter(~Q(testType='2'), ~Q(testType='3'), ~Q(testType='8'),
~Q(testType='15')) # !warning:中变量:第一轮动态测试的测试项
summary_r2_demand_info, summry_r2_demandType_info = create_demand_summary(demand_r2_dynamic_qs,
project_obj.ident)
# 8.第二轮所有动态问题统计
problems_dynamic_r2 = problems_r2.filter(~Q(case__test__testType='2'), ~Q(case__test__testType='3'),
~Q(case__test__testType='8'),
~Q(case__test__testType='15')) # !critical:大变量:第一轮动态问题单qs
problem_dynamic_r2_type_str = create_problem_type_str(problems_dynamic_r2)
problem_dynamic_r2_grade_str = create_problem_grade_str(problems_dynamic_r2)
r2_dynamic_str = "本轮测试未发现新问题。"
has_r2_dynamic = False
if len(problems_dynamic_r2) > 0:
has_r2_dynamic = True
r2_dynamic_str = (f"{round_chinese[int(round_id)]}轮动态测试共发现问题{problems_dynamic_r2.count()}个,"
f"其中{problem_dynamic_r2_type_str}"
f"{problem_dynamic_r2_grade_str}")
context = {
'project_name': project_obj.name,
'r1_so_version': r1_so_dut.version,
'so_version': so_dut.version,
'case_dynamic_r2_count': case_r2_qs.count(),
'dynamic_testType_list': ''.join(testType_list),
'dynamic_testType_count': testType_count,
'r2_exe_info_all': summary_r2_demand_info,
'r2_exe_info_type': summry_r2_demandType_info,
'has_r2_dynamic': has_r2_dynamic,
'r2_dynamic_str': r2_dynamic_str,
'round_id': round_chinese[round_id],
}
return context

View File

@@ -0,0 +1,207 @@
# 本模块主要以项目开始时间、结束时间、轮次开始时间、结束时间计算文档中的各个时间
from datetime import timedelta, date
from apps.project.models import Project
from django.shortcuts import get_object_or_404
from ninja.errors import HttpError # 从代码抛出该异常被ninja截取变为response
def format_remove_heng(dateT: date) -> str:
"""该函数将date对象的横杠-去掉输出str"""
return str(dateT).replace('-', '')
def times_by_cover_time(cover_time: date) -> dict:
"""该函数为每个产品文档根据封面时间,渲染签署页时间、文档变更记录时间"""
return {
'preparation_time_no_format': cover_time - timedelta(days=2),
'preparation_time': format_remove_heng(cover_time - timedelta(days=2)), # 拟制时间:为编制结束时间-2天
'inspect_time': format_remove_heng(cover_time - timedelta(days=1)), # 校对时间:为编制时间+1天
'auditing_time': format_remove_heng(cover_time),
'ratify_time': format_remove_heng(cover_time),
'create_doc_time': format_remove_heng(cover_time - timedelta(days=2)),
'doc_v1_time': format_remove_heng(cover_time)
}
class DocTime:
def __init__(self, project_id: int):
self.project = get_object_or_404(Project, id=project_id)
# 用户录入时间-项目
self.p_start = self.project.beginTime # 被测件接收时间/
self.p_end = self.project.endTime # 大纲测评时间周期结束时间/
# 遍历轮次时间-多个
self.round_count = self.project.pField.count()
self.round_time = [] # 轮次按顺序排序
for round in self.project.pField.all():
self.round_time.append({
'start': round.beginTime,
'end': round.endTime,
'location': round.location
})
# ~~~~由上面时间二次计算得出时间~~~~ -> TODO:可由用户设置间隔时间!!!!
self.dg_bz_start = self.p_start + timedelta(days=1) # 大纲编制开始时间,项目开始时间+1天
self.dg_bz_end = self.dg_bz_start + timedelta(days=6) # 大纲编制结束时间,大纲编制开始+6天
self.test_sj_start = self.dg_bz_end + timedelta(days=1) # 测评设计与实现时间,在大纲编制结束+1天
self.test_sj_end = self.test_sj_start + timedelta(days=5) # 测评设计与实现结束,在开始+5天
# ~~~~储存每个文档的cover_time~~~~
self.dg_cover_time = self.dg_bz_end
self.sm_cover_time = self.test_sj_end
self.jl_cover_time = self.round_time[0]['end']
self.wtd_cover_time = self.round_time[-1]['end']
# 该函数生成大纲文档片段-测评时间和地点的时间和地点信息
def dg_address_time(self):
"""直接返回context去渲染"""
# 需要判断round_time是否有值
if len(self.round_time) <= 0:
raise HttpError(status_code=400, message='您还未创建轮次时间,请填写后生成')
return {
'start_year': self.p_start.year,
'start_month': self.p_start.month,
'end_year': self.p_end.year,
'end_month': self.p_end.month,
'beginTime_strf': format_remove_heng(self.p_start),
'dgCompileStart': format_remove_heng(self.dg_bz_start),
'dgCompileEnd': format_remove_heng(self.dg_bz_end),
'designStart': format_remove_heng(self.test_sj_start),
'designEnd': format_remove_heng(self.test_sj_end),
'location': self.round_time[0]['location']
}
# 该函数生成报告文档片段-测评时间和地点【注意使用了dg_address_time -> 所以后续有修改注意前导】
def bg_address_time(self):
if len(self.round_time) <= 0:
raise HttpError(status_code=400, message='您还未创建轮次时间,请填写后生成')
# 先使用大纲的时间行数作为前三行
cname = ['首轮测试', '第二轮测试', '第三轮测试', '第四轮测试', '第五轮测试', '第六轮测试', '第七轮测试',
'第八轮测试', '第九轮测试', '第十轮测试']
dg_address_time = self.dg_address_time()
round_time_list = []
index = 0
for round_dict in self.round_time:
one_dict = {
'name': cname[index],
'start': format_remove_heng(round_dict['start']),
'end': format_remove_heng(round_dict['end']),
'location': round_dict['location']
}
index += 1
round_time_list.append(one_dict)
return {
'begin_year': dg_address_time['start_year'],
'begin_month': dg_address_time['start_month'],
'end_year': dg_address_time['end_year'],
'end_month': dg_address_time['end_month'],
'begin_time': dg_address_time['beginTime_strf'],
'dg_weave_start_date': dg_address_time['dgCompileStart'],
'dg_weave_end_date': dg_address_time['dgCompileEnd'],
'sj_weave_start_date': dg_address_time['designStart'],
'sj_weave_end_date': dg_address_time['designEnd'],
'round_time_list': round_time_list,
# 测评总结 -> 依据项目结束时间-7 ~ 项目结束时间
'summary_start_date': format_remove_heng(self.p_end - timedelta(days=7)),
'summary_end_date': format_remove_heng(self.p_end),
}
# 生成报告中测评完成情况 -> 必须依据其他内容生成时间【注意使用了bg_address_time -> 所以后续有修改注意前导】
def bg_completion_situation(self):
bg_timer_dict = self.bg_address_time()
xq_fx_time_end = self.dg_bz_start + timedelta(days=2)
ch_time_start = xq_fx_time_end + timedelta(days=1)
ch_time_end = self.dg_bz_end
if len(self.round_time) < 1:
raise HttpError(status_code=400, message='您还未创建第一轮测试的时间,请填写后再生成')
return {
'start_time_year': bg_timer_dict['begin_year'],
'start_time_month': bg_timer_dict['begin_month'],
'xq_fx_time_start_year': self.dg_bz_start.year,
'xq_fx_time_start_month': self.dg_bz_start.month,
'xq_fx_time_start_day': self.dg_bz_start.day,
'xq_fx_time_end_year': xq_fx_time_end.year, # 需求分析结束时间是大纲编制开始+2
'xq_fx_time_end_month': xq_fx_time_end.month,
'xq_fx_time_end_day': xq_fx_time_end.day,
'ch_start_year': ch_time_start.year,
'ch_start_month': ch_time_start.month,
'ch_start_day': ch_time_start.day,
'ch_end_year': ch_time_end.year,
'ch_end_month': ch_time_end.month,
'ch_end_day': ch_time_end.day,
'sj_start_year': self.test_sj_start.year,
'sj_start_month': self.test_sj_start.month,
'sj_start_day': self.test_sj_start.day,
'sj_end_year': self.test_sj_end.year,
'sj_end_month': self.test_sj_end.month,
'sj_end_day': self.test_sj_end.day,
'end_time_year': self.p_end.year,
'end_time_month': self.p_end.month,
'exec_start_time_year': self.round_time[0]['start'].year,
'exec_start_time_month': self.round_time[0]['start'].month,
'exec_start_time_day': self.round_time[0]['start'].day,
'exec_end_time_year': self.round_time[0]['end'].year,
'exec_end_time_month': self.round_time[0]['end'].month,
'exec_end_time_day': self.round_time[0]['end'].day,
}
# 该函数生成最终大纲的时间
def dg_final_time(self):
cover_time = self.dg_bz_end
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
# 新增给大纲模版10.2章节context
context.update(basic_line1=cover_time.strftime("%Y年%m月"), basic_line2=self.p_end.strftime("%Y年%m月"))
# 新增给大纲模版10.3.2章节的context
sm_context = self.sm_final_time()
context.update(sm_end_time=sm_context['preparation_time_no_format'].strftime("%Y年%m月"))
return context
# 该函数生成说明文档的时间 -> 依据项目时间而非用户第一轮填写时间!
def sm_final_time(self):
cover_time = self.test_sj_end # 封面时间:为大纲时间中“测评设计与实现”结束时间
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
return context
# 该函数生成记录文档的时间 -> 依据第一轮测试用户填写的事件
def jl_final_time(self):
if len(self.round_time) < 1:
raise HttpError(status_code=400, message='您还未创建第一轮测试的时间,请填写后再生成')
cover_time = self.round_time[0]['end'] # 封面时间为用户填写第一轮结束时间
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
return context
# 问题单的时间 -> 依据最后一轮次的结束时间+1天
def wtd_final_time(self):
if len(self.round_time) < 1:
raise HttpError(status_code=400, message='您还未创建第一轮测试的时间,请填写后再生成')
cover_time = self.round_time[-1]['end']
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
return context
# 回归测试说明时间 -> 根据第二轮、第三轮...的开始时间
def hsm_final_time(self, round_key: str):
if len(self.round_time) < int(round_key) + 1:
raise HttpError(status_code=400, message='您填写的回归轮次时间不正确,请填写后再生成')
cover_time = self.round_time[int(round_key)]['start']
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
return context
# 回归测试记录时间 -> 根据第二轮、第三轮...的结束时间
def hjl_final_time(self, round_key: str) -> dict:
if len(self.round_time) < int(round_key) + 1:
raise HttpError(status_code=400, message='您填写的回归轮次时间不正确,请填写后再生成')
cover_time = self.round_time[int(round_key)]['end']
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
return context
# 生成报告非过程时间 -> 根据项目结束时间来定
def bg_final_time(self) -> dict:
if len(self.round_time) <= 0:
raise HttpError(status_code=400, message='您还未创建轮次时间,请填写后生成')
cover_time = self.p_end
# 这里做判断,如果项目结束时间/最后一轮结束时间
if cover_time < self.round_time[-1]['end']:
raise HttpError(500, message='项目结束时间早于最后一轮次结束时间或等于开始时间,请修改项目结束时间')
context = times_by_cover_time(cover_time)
context.update(cover_time=cover_time.strftime("%Y年%m月%d"))
return context

View File

@@ -0,0 +1,21 @@
from abc import ABC
from apps.project.models import Project
from django.shortcuts import get_object_or_404
from apps.dict.models import Fragment
from apps.createDocument.extensions.parse_rich_text import RichParser
class FragementToolsMixin(ABC):
"""该混合主要给文档片段进行功能封装"""
def _generate_frag(self, id: int, doc, doc_name: str):
"""传入项目id/"""
project_qs = get_object_or_404(Project, id=id)
replace = False # 是否替换标志
rich_text_list = []
# [deprecated]判断是否有当前项目的文档片段
fragments = project_qs.frag.all()
# 传入'片段名称'和判断is_main
frag: Fragment = fragments.filter(name=doc_name, is_main=True).first()
if frag:
replace = True
rich_text_list = RichParser(frag.content).get_final_format_list(doc)
return replace, frag, rich_text_list

View File

@@ -0,0 +1,127 @@
"""
专门解析富文本插件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

View File

@@ -0,0 +1,35 @@
from apps.project.models import Problem
from utils.util import get_str_dict
from apps.createDocument.extensions.parse_rich_text import RichParser
def parse_html(html_txt, a_list, doc):
"""解析HTML字段的文字和图片到列表,输入是HTML字段的txt以及列表输出列表"""
parser = RichParser(html_txt)
a_list.extend(parser.get_final_list(doc, img_size=80))
return a_list
def create_one_problem_dit(problem: Problem, problem_prefix: str, doc) -> dict:
"""问题单汇总表每个问题作为一行的数据"""
problem_dict = {
'ident': '_'.join([problem_prefix, problem.ident]),
'grade': get_str_dict(problem.grade, 'problemGrade'),
'type': get_str_dict(problem.type, 'problemType'),
'status': get_str_dict(problem.status, 'problemStatu')
}
# 问题操作 - HTML解析
desc_list = ['【问题描述】']
desc_list = parse_html(problem.operation, desc_list, doc)
desc_list_yq = [f'\a【问题影响】\a{problem.result}']
desc_list.extend(desc_list_yq)
problem_dict['desciption'] = desc_list
# 问题处理方式表格单元格
solve_list = [f'【原因分析】\a{problem.analysis}']
solve_list_effect = [f'\a【影响域】\a{problem.effect_scope}']
solve_list_basic = ['\a【处理方式】', problem.solve]
solve_list_verify = ['\a【回归验证】']
solve_list_verify = parse_html(problem.verify_result, solve_list_verify, doc)
solve_list.extend(solve_list_effect)
solve_list.extend(solve_list_basic)
solve_list.extend(solve_list_verify)
problem_dict['solve'] = solve_list
return problem_dict

View File

@@ -0,0 +1,97 @@
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()

View File

@@ -0,0 +1,99 @@
from apps.project.models import Project
from utils.chapter_tools.csx_chapter import create_csx_chapter_dict
from utils.util import get_testType, get_case_ident
# 传入项目对象、dut的类型例如'XQ'、round_str字符串表示例如第一轮为'XQ',测试项其实章节前缀例如
def create_bg_round1_zhui(project_obj: Project, dut_str='XQ', round_str='0'):
"""传入项目对象,返回{仅第一轮}的design_list渲染到模版的列表"""
# 首先定义后面用问题单前缀
problem_prefix = "".join(['PT_', project_obj.ident, '_'])
# 如果是第一轮测试项章节号前缀则为6.2其他轮次为4.1
demand_prefix = '6.2' if round_str == '0' else "3.1"
design_list = []
round_obj = project_obj.pField.filter(key=round_str).first() # 轮次对象
case_index = 1
if round_obj:
testType_list, last_chapter_items = create_csx_chapter_dict(round_obj)
specific_dut = round_obj.rdField.filter(type=dut_str).first() # design的列表
if dut_str == 'XQ':
so_dut = round_obj.rdField.filter(type='SO').first()
if so_dut:
designs = so_dut.rsField.all()
for design in designs:
design_dict = {'name': "/", 'chapter': "/", 'test_demand': []}
# 获取一个design的所以测试项包括关联测试项
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
key_index = int(test_item.key.split("-")[-1]) + 1
test_index = str(key_index).rjust(3, '0')
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"),
test_item.ident, test_index])
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([demand_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'case_list': []}
for case in test_item.tcField.all():
# 用例如果关联了问题单,那么直接判断未通过,如果没有关联问题单,则找步骤里面是否有未执行
# 如果未执行,不显示未执行,显示“/”斜杠
is_passed = '通过'
problem_ident_list = []
for problem in case.caseField.all():
problem_ident_list.append("".join([problem_prefix, problem.ident]))
if len(problem_ident_list) > 0:
is_passed = '未通过'
case_dict = {
'index': case_index,
'name': case.name,
'ident': get_case_ident(reveal_ident, case),
'passed': is_passed,
'problem_ident_list': "\a".join(problem_ident_list)
}
test_item_dict['case_list'].append(case_dict)
case_index += 1
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
if specific_dut:
designs = specific_dut.rsField.all()
for design in designs:
design_dict = {'name': design.name, 'chapter': design.chapter, 'test_demand': []}
# 获取一个design的所以测试项包括关联测试项
test_items = []
test_items.extend(design.dtField.all())
test_items.extend(design.odField.all())
for test_item in test_items:
key_index = int(test_item.key.split("-")[-1]) + 1
test_index = str(key_index).rjust(3, '0')
reveal_ident = "_".join(
["XQ", get_testType(test_item.testType, "testType"),
test_item.ident, test_index])
test_item_last_chapter = last_chapter_items[test_item.testType].index(test_item.key) + 1
test_chapter = ".".join([demand_prefix, str(testType_list.index(test_item.testType) + 1),
str(test_item_last_chapter)])
test_item_dict = {'name': test_item.name, 'chapter': test_chapter, 'ident': reveal_ident,
'case_list': []}
for case in test_item.tcField.all():
# 用例如果关联了问题单,那么直接判断未通过,如果没有关联问题单,则找步骤里面是否有未执行
# 如果未执行,不显示未执行,显示“/”斜杠
is_passed = '通过'
problem_ident_list = []
for problem in case.caseField.all():
problem_ident_list.append("".join([problem_prefix, problem.ident]))
if len(problem_ident_list) > 0:
is_passed = '未通过'
case_dict = {
"index": case_index,
'name': case.name,
'ident': get_case_ident(reveal_ident, case),
'passed': is_passed,
'problem_ident_list': "\a".join(problem_ident_list)
}
test_item_dict['case_list'].append(case_dict)
case_index += 1
design_dict['test_demand'].append(test_item_dict)
design_list.append(design_dict)
return design_list

View File

@@ -0,0 +1 @@
from django.db import models

View File

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1 @@
from django.shortcuts import render

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CreateseitaidocumentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.createSeiTaiDocument'

View File

@@ -0,0 +1,493 @@
from pathlib import Path
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from utils.path_utils import project_path
from ninja import File, UploadedFile
from ninja.errors import HttpError
from ninja_extra.controllers import api_controller, ControllerBase, route
from ninja_jwt.authentication import JWTAuth
from ninja_extra.permissions import IsAuthenticated
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.db.models import QuerySet
from docx import Document
from docxtpl import DocxTemplate
# 工具
from apps.createSeiTaiDocument.docXmlUtils import generate_temp_doc, get_frag_from_document
from apps.createSeiTaiDocument.schema import SeitaiInputSchema
from utils.chen_response import ChenResponse
from apps.project.models import Project, Dut
from apps.createDocument.extensions.documentTime import DocTime
from utils.util import get_str_dict
from apps.createSeiTaiDocument.extensions.download_response import get_file_respone
# 图片工具docx
from apps.createSeiTaiDocument.extensions.shape_size_tool import set_shape_size
# 修改temp文本片段工具
from apps.createSeiTaiDocument.docXmlUtils import get_jinja_stdContent_element, stdContent_modify
main_download_path = Path(settings.BASE_DIR) / 'media'
# @api_controller("/create", tags=['生成产品文档接口'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/create", tags=['生成产品文档接口'])
class GenerateSeitaiController(ControllerBase):
chinese_round_name: list = ['', '', '', '', '', '', '', '', '', '']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.project_obj: Project | None = None
self.temp_context = {}
@route.post("/dgDocument", url_name="create-dgDocument")
@transaction.atomic
def create_dgDocument(self, payload: SeitaiInputSchema):
# 获取项目Model
self.project_obj = get_object_or_404(Project, id=payload.id)
# 生成大纲需要的文本片段信息储存在字典里面
sec_title = get_str_dict(self.project_obj.secret, 'secret')
duty_person = self.project_obj.duty_person
is_jd = True if self.project_obj.report_type == '9' else False
self.temp_context = {
'is_jd': is_jd,
'jd_or_third': "鉴定" if is_jd else "第三方",
'project_ident': self.project_obj.ident,
'project_name': self.project_obj.name,
'test_purpose': "装备鉴定和列装定型" if is_jd else "软件交付和使用",
'sec_title': sec_title,
'sec': sec_title,
'duty_person': duty_person,
'member': self.project_obj.member[0] if len(
self.project_obj.member) > 0 else duty_person,
'entrust_unit': self.project_obj.entrust_unit
} | DocTime(payload.id).dg_final_time() # python3.9以上推荐使用|运算符合并
# 调用self添加temp_context信息
self.get_first_round_code_ident()
self.get_xq_doc_informations()
result = generate_temp_doc('dg', payload.id, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(status=400, code=400, message=result.get('msg', 'dg未报出错误原因反正在生成文档出错'))
dg_replace_path, dg_seitai_final_path = result
# ~~~~start2025/04/19-新增渲染单个字段(可能封装为函数-对temp文件下的jinja字段处理~~~~
# 现在已经把alias和stdContent对应起来了
text_frag_name_list, doc_docx = get_jinja_stdContent_element(dg_replace_path)
# 遍历找出来的文本片段进行修改
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
# ~~~~end~~~~
try:
doc_docx.save(dg_seitai_final_path)
# 文件下载
return get_file_respone(payload.id, '测评大纲')
except PermissionError as e:
return ChenResponse(status=400, code=400, message="文档未生成或生成错误!,{0}".format(e))
@route.post('/smDocument', url_name='create-smDocument')
@transaction.atomic
def create_smDocument(self, payload: SeitaiInputSchema):
"""生成最后说明文档"""
# 获取项目对象
self.project_obj = get_object_or_404(Project, id=payload.id)
# 首先第二层模版所需变量
is_jd = True if self.project_obj.report_type == '9' else False
self.temp_context = {
'project_name': self.project_obj.name,
'project_ident': self.project_obj.ident,
'is_jd': is_jd,
'jd_or_third': "鉴定" if is_jd else "第三方",
'ident': self.project_obj.ident,
'sec_title': get_str_dict(self.project_obj.secret, 'secret'),
'sec': get_str_dict(self.project_obj.secret, 'secret'),
'duty_person': self.project_obj.duty_person,
'member': self.project_obj.member[0] if len(
self.project_obj.member) > 0 else self.project_obj.duty_person,
} | DocTime(payload.id).sm_final_time()
self.get_first_round_code_ident()
# 文档片段操作
result = generate_temp_doc('sm', payload.id, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(code=400, status=400, message=result.get('msg', '无错误原因'))
sm_to_tpl_file, sm_seitai_final_file = result
# 文本片段操作
text_frag_name_list, doc_docx = get_jinja_stdContent_element(sm_to_tpl_file)
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
# 注册时间变量
try:
doc_docx.save(sm_seitai_final_file)
return get_file_respone(payload.id, '测试说明')
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
@route.post('/jlDocument', url_name='create-jlDocument')
@transaction.atomic
def create_jlDocument(self, payload: SeitaiInputSchema):
self.project_obj = get_object_or_404(Project, id=payload.id)
# seitai文档所需变量
is_jd = True if self.project_obj.report_type == '9' else False
member = self.project_obj.member[0] if len(self.project_obj.member) > 0 else self.project_obj.duty_person
self.temp_context = {
'project_name': self.project_obj.name,
'project_ident': self.project_obj.ident,
'is_jd': is_jd,
'name': self.project_obj.name,
'ident': self.project_obj.ident,
'sec_title': get_str_dict(self.project_obj.secret, 'secret'),
'duty_person': self.project_obj.duty_person, 'member': member
} | DocTime(payload.id).jl_final_time()
self.get_xq_doc_informations() # 添加文本片段“xq_version”
result = generate_temp_doc('jl', payload.id, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(code=400, status=400, message=result.get('msg', '无错误原因'))
jl_to_tpl_file, jl_seitai_final_file = result
text_frag_name_list, doc_docx = get_jinja_stdContent_element(jl_to_tpl_file)
# 文本片段操作
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
# 重新设置时序图大小
for shape in doc_docx.inline_shapes:
set_shape_size(shape)
try:
doc_docx.save(jl_seitai_final_file)
return get_file_respone(payload.id, '测试记录')
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
@route.post('/hsmDocument', url_name='create-hsmDocument')
@transaction.atomic
def create_hsmDocument(self, payload: SeitaiInputSchema):
"""生成最后的回归测试说明-(多个文档)"""
self.project_obj = get_object_or_404(Project, id=payload.id)
hround_list: QuerySet = self.project_obj.pField.exclude(key='0') # 非第一轮次
cname_list = []
if len(hround_list) < 1:
return ChenResponse(code=400, status=400, message='无回归轮次,请添加后再生成')
for hround in hround_list:
# 获取当前轮次中文数字
cname = self.chinese_round_name[int(hround.key)]
# 将cname存入一个list以便后续拼接给下载函数
cname_list.append(cname)
is_jd = True if self.project_obj.report_type == '9' else False
member = self.project_obj.member[0] if len(self.project_obj.member) > 0 else self.project_obj.duty_person
# 回归轮次的标识和版本
so_dut: Dut = hround.rdField.filter(type='SO').first()
if not so_dut:
return ChenResponse(status=400, code=400, message=f'您缺少第{cname}轮的源代码被测件')
# 每次循环会更新temp_context
self.temp_context = {
'project_name': self.project_obj.name,
'project_ident': self.project_obj.ident,
'is_jd': is_jd,
'sec_title': get_str_dict(self.project_obj.secret, 'secret'),
'duty_person': self.project_obj.duty_person,
'member': member,
'round_num': str(int(hround.key) + 1),
'round_num_chn': cname,
'soft_ident': so_dut.ref,
'soft_version': so_dut.version,
'location': hround.location,
} | DocTime(payload.id).hsm_final_time(hround.key)
# 注意回归测试说明、回归测试记录都生成多个文档
result = generate_temp_doc('hsm', payload.id, round_num=cname, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(status=400, code=400,
message=result.get('msg', '回归测试说明生成报错...'))
hsm_replace_path, hsm_seitai_final_path = result
text_frag_name_list, doc_docx = get_jinja_stdContent_element(hsm_replace_path)
# 文本片段操作
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
try:
doc_docx.save(hsm_seitai_final_path)
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 因为回归说明、回归记录可能有多份多份下载zip否则docx
if len(cname_list) == 1:
return get_file_respone(payload.id, '第二轮回归测试说明')
else:
return get_file_respone(payload.id, list(map(lambda x: f"{x}轮回归测试说明", cname_list)))
@route.post('/hjlDocument', url_name='create-hjlDocument')
@transaction.atomic
def create_hjlDocument(self, payload: SeitaiInputSchema):
"""生成最后的回归测试记录-(多个文档)"""
self.project_obj: Project = get_object_or_404(Project, id=payload.id)
# 取非第一轮次
hround_list: QuerySet = self.project_obj.pField.exclude(key='0')
cname_list = []
if len(hround_list) < 1:
return ChenResponse(code=400, status=400, message='无回归测试轮次,请创建后再试')
for hround in hround_list:
# 取出当前轮次key减1就是上一轮次
cname = self.chinese_round_name[int(hround.key)] # 输出二、三...
cname_list.append(cname)
member = self.project_obj.member[0] if len(self.project_obj.member) > 0 else self.project_obj.duty_person
is_jd = True if self.project_obj.report_type == '9' else False
so_dut: Dut = hround.rdField.filter(type='SO').first()
if not so_dut:
return ChenResponse(status=400, code=400, message=f'您缺少第{cname}轮的源代码被测件')
self.temp_context = {
'project_name': self.project_obj.name,
'project_ident': self.project_obj.ident,
'is_jd': is_jd,
'sec_title': get_str_dict(self.project_obj.secret, 'secret'),
'duty_person': self.project_obj.duty_person,
'member': member,
'round_num': str(int(hround.key) + 1),
'round_num_chn': cname,
'soft_ident': so_dut.ref,
'soft_version': so_dut.version,
} | DocTime(payload.id).hjl_final_time(hround.key)
result = generate_temp_doc('hjl', payload.id, round_num=cname, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(status=400, code=400,
message=result.get('msg', '回归测试记录生成错误!'))
hjl_replace_path, hjl_seitai_final_path = result
text_frag_name_list, doc_docx = get_jinja_stdContent_element(hjl_replace_path)
# 文本片段操作
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
# 重新设置时序图大小(注意不变)
for shape in doc_docx.inline_shapes:
set_shape_size(shape)
try:
doc_docx.save(hjl_seitai_final_path)
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
if len(cname_list) == 1:
return get_file_respone(payload.id, '第二轮回归测试记录')
else:
return get_file_respone(payload.id, list(map(lambda x: f"{x}轮回归测试说明", cname_list)))
@route.post('/wtdDocument', url_name='create-wtdDocument')
@transaction.atomic
def create_wtdDocument(self, payload: SeitaiInputSchema):
"""生成最后的问题单"""
self.project_obj = get_object_or_404(Project, id=payload.id)
# seitai文档所需变量
member = self.project_obj.member[0] if len(self.project_obj.member) > 0 else self.project_obj.duty_person
is_jd = True if self.project_obj.report_type == '9' else False
self.temp_context = {
"project_name": self.project_obj.name,
'project_ident': self.project_obj.ident,
'is_jd': is_jd,
'member': member,
'duty_person': self.project_obj.duty_person,
'sec_title': get_str_dict(self.project_obj.secret, 'secret'),
} | DocTime(payload.id).wtd_final_time()
result = generate_temp_doc('wtd', payload.id, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(status=400, code=400, message=result.get('msg', 'wtd未报出错误原因反正在生成文档出错'))
wtd_replace_path, wtd_seitai_final_path = result
text_frag_name_list, doc_docx = get_jinja_stdContent_element(wtd_replace_path)
# 文本片段操作
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
try:
doc_docx.save(wtd_seitai_final_path)
return get_file_respone(payload.id, '问题单')
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
@route.post('/bgDocument', url_name='create-bgDocument')
@transaction.atomic
def create_bgDocument(self, payload: SeitaiInputSchema):
"""生成最后的报告文档"""
self.project_obj = get_object_or_404(Project, id=payload.id)
# seitai文档所需变量
## 1.判断是否为JD
member = self.project_obj.member[0] if len(self.project_obj.member) > 0 else self.project_obj.duty_person
is_jd = True if self.project_obj.report_type == '9' else False
self.temp_context = {
'project_name': self.project_obj.name,
'project_ident': self.project_obj.ident,
'test_purpose': "装备鉴定和列装定型" if is_jd else "软件交付和使用",
'is_jd': is_jd,
'sec_title': get_str_dict(self.project_obj.secret, 'secret'),
'duty_person': self.project_obj.duty_person,
'jd_or_third': "鉴定" if is_jd else "第三方",
'entrust_unit': self.project_obj.entrust_unit,
'member': member,
'joined_part': f'{self.project_obj.dev_unit}军事代表室、{self.project_obj.dev_unit}',
} | DocTime(payload.id).bg_final_time()
result = generate_temp_doc('bg', payload.id, frag_list=payload.frag)
if isinstance(result, dict):
return ChenResponse(status=400, code=400, message=result.get('msg', 'bg未报出错误原因反正在生成文档出错'))
bg_replace_path, bg_seitai_final_path = result
text_frag_name_list, doc_docx = get_jinja_stdContent_element(bg_replace_path)
# 文本片段操作
self.text_frag_replace_handle(text_frag_name_list, doc_docx)
try:
doc_docx.save(bg_seitai_final_path)
return get_file_respone(payload.id, '测评报告')
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# ~~~~模版设计模式~~~~
# 1.传入sdtContent列表和替换后的文档对象进行替换操作
def text_frag_replace_handle(self, text_frag_name_list, doc_docx: Document):
for text_frag in text_frag_name_list:
alias = text_frag['alias']
if alias in self.temp_context:
sdtContent = text_frag['sdtContent']
stdContent_modify(self.temp_context[alias], doc_docx, sdtContent)
else:
print('未查找的文本片段变量:', alias)
# ~~~~下面是生成文档辅助文本片段取变量,统一设置报错信息等,后续看重复代码封装~~~~
# self拥有变量self.project_obj / self.temp_context(用于替换文本片段的字典)
# 1.获取项目第一轮round的源代码dut的用户标识/版本/第一轮测试地点
def get_first_round_code_ident(self):
round_obj = self.project_obj.pField.filter(key='0').first()
if round_obj:
self.temp_context.update({
'location': round_obj.location,
})
code_dut_obj = round_obj.rdField.filter(type='SO').first()
if code_dut_obj:
self.temp_context.update({
'soft_ident': code_dut_obj.ref,
'soft_version': code_dut_obj.version,
})
return
raise HttpError(500, "第一轮次未创建,或第一轮动态测试地点为填写,或源代码被测件未创建,请先创建")
# 2.获取第一轮次需求规格说明dut
def get_xq_doc_informations(self):
round1_xq_dut = self.project_obj.pdField.filter(round__key='0', type='XQ').first()
if round1_xq_dut:
self.temp_context.update({'xq_version': round1_xq_dut.version})
return
raise HttpError(500, "第一轮次被测件:需求规格说明可能未创建,生成文档失败")
# documentType - 对应的目录名称
documentType_to_dir = {
'测评大纲': '',
'测试说明': 'sm',
'测试记录': 'jl',
'回归测试说明': 'hsm',
'回归测试记录': 'hjl',
'问题单': 'wtd',
'测评报告': 'bg'
}
# 处理文档片段相关请求
@api_controller('/createfragment', tags=['生成文档-文档片段接口集合'])
class CreateFragmentController(ControllerBase):
@route.get("/get_fragments", url_name='get-fragments')
def get_fragements(self, id: int, documentType: str):
"""根据项目id和文档类型获取有哪些文档片段"""
# 获取文档片段的字符串列表
frags = self.get_fragment_name_by_document_name(id, documentType)
# 如果没有文档片段-说明没有生成二段文档
if not frags:
return ChenResponse(status=500, code=500, message='文档片段还未生成,请关闭后再打开/或者先下载基础文档')
# 到这里说fragments_files数组有值返回文件名数组
return ChenResponse(data=[fragment for fragment in frags], message='返回文档片段成功')
@staticmethod
def get_fragment_name_by_document_name(id: int, document_name: str):
# 1.找到模版的路径 - 不用异常肯定存在
document_path = main_download_path / project_path(id) / 'form_template' / 'products' / f"{document_name}.docx"
# 2.识别其中的文档片段
frag_list = get_frag_from_document(document_path)
# 3.这里处理报告里第十轮次前端展示问题
## 3.1先判断是否为报告
if document_name == '测评报告':
## 3.2然后判断有几个轮次
project_obj = get_object_or_404(Project, id=id)
round_qs = project_obj.pField.all()
white_list_frag = []
## 3.3将希望有的片段名称加入白名单
for round_obj in round_qs:
chn_num = digit_to_chinese(int(round_obj.key) + 1)
exclude_str = f"测试内容和结果_第{chn_num}轮次" # 组成识别字符串
white_list_frag.append(exclude_str)
## 3.4过滤包含“测试内容和结果的轮次在白名单的通过”
# 去掉所有“测试内容和结果_”的片段
filter_frags = list(filter(lambda x: '测试内容和结果' not in x['frag_name'], frag_list))
# 再找到白名单的“测试内容和结果_”的片段
content_and_result_frags = list(
filter(lambda x: '测试内容和结果' in x['frag_name'] and x['frag_name'] in white_list_frag, frag_list))
# 再组合起来返回
filter_frags.extend(content_and_result_frags)
return filter_frags
return frag_list
@route.get("/get_round_exit", url_name='get-round-exit')
def get_round_exit(self, id: int):
"""该函数主要识别有几轮回归测试说明、几轮回归测试记录"""
project_obj: Project = get_object_or_404(Project, id=id)
# 取非第一轮次的轮次的个数
round_count = project_obj.pField.exclude(key='0').count()
return {'count': round_count}
# 自定义修改Django的文件系统-启动覆盖模式
class OverwriteStorage(FileSystemStorage):
def __init__(self, *args, **kwargs):
kwargs['allow_overwrite'] = True # 启用覆盖模式
super().__init__(*args, **kwargs)
def digit_to_chinese(num):
num_dict = {'0': '', '1': '', '2': '', '3': '', '4': '',
'5': '', '6': '', '7': '', '8': '', '9': '', '10': ''}
return ''.join(num_dict[d] for d in str(num))
# 处理用户上传有文档片段的产品文档文件:注意回归测试说明、回归测试记录需要单独处理
@api_controller('/documentUpload', tags=['生成文档-上传模版文档接口'])
class UploadDocumentController(ControllerBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 储存上传文件
self.upload_file: UploadedFile | None = None
@route.post("/file", url_name='upload-file')
def upload_file(self, id: int, documentType: str, file: File[UploadedFile], round_num: int = None):
self.upload_file = file
# 1.获取储存路径
target_dir = main_download_path / project_path(id) / 'form_template' / 'products'
# 2.初始化文件系统
fs = OverwriteStorage(location=target_dir)
# 新:如果是大纲片段,则大纲所有片段以文档片段方式储存在/reuse文件夹下面
if documentType == '测评大纲':
self.get_dg_to_reuse_dir(target_dir.parent.parent / 'reuse')
if round_num is None:
# 处理非“回归测试说明”/“回归测试记录”文档的上传
# warning不校验文档内是否有文档片段由用户保证上传内容
# 3.覆盖储存注意会返回文件的name属性
fs.save(f"{documentType}.docx", self.upload_file)
else:
# 处理“回归测试说明”/“回归测试记录”文档的上传
fs.save(f"{digit_to_chinese(round_num)}{documentType}.docx", self.upload_file)
return ChenResponse(status=200, code=200, message=f'上传{documentType}成功!')
# 主功能函数将所有大纲的片段储存在reuse下面以便其他文件使用
def get_dg_to_reuse_dir(self, reuse_dir_path: Path):
"""将大纲的文档片段储存在/reuse文件夹下面"""
doc = Document(self.upload_file)
frag_list = self.get_document_frag_list(doc)
for frag_item in frag_list:
# 目的是格式明确按照“测评大纲.docx”进行后续文档一样必须按照这样
new_doc = Document((reuse_dir_path / 'basic_doc.docx').as_posix())
if frag_item['content'] is not None:
# XML元素可以直接append
new_doc.element.body.clear_content()
for frag_child in frag_item['content'].iterchildren():
new_doc.element.body.append(frag_child)
filename = f"{frag_item['alias']}.docx"
new_doc.save((reuse_dir_path / filename).as_posix())
# 辅助函数:将上传文件的文档片段以列表形式返回
def get_document_frag_list(self, doc: Document):
body = doc.element.body
sdt_element_list = body.xpath('./w:sdt') # 只查询文档片段,非文本片段
frag_list = []
for sdt_element in sdt_element_list:
alias_name = None
sdtContent = None
for sdt_child in sdt_element.iterchildren():
if sdt_child.tag.endswith('sdtPr'):
for sdtPr_child in sdt_child.getchildren():
if sdtPr_child.tag.endswith('alias'):
if len(sdtPr_child.attrib.values()) > 0:
alias_name = sdtPr_child.attrib.values()[0]
if sdt_child.tag.endswith("sdtContent"):
sdtContent = sdt_child
frag_list.append({'alias': alias_name, 'content': sdtContent})
return list(filter(lambda x: x['alias'] is not None, frag_list))

View File

@@ -0,0 +1,348 @@
"""该文件是:替换文档片段然后生成辅助生成最终文档"""
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

View File

@@ -0,0 +1,38 @@
import os, io
from typing import List
import zipfile
from pathlib import Path
from django.conf import settings
from utils.path_utils import project_path
from utils.chen_response import ChenResponse
from django.http import FileResponse, HttpResponse
main_download_path = Path(settings.BASE_DIR) / 'media'
def get_file_respone(id: int, file_name: str | List[str]):
"""将生成文档下载响应"""
# 1.如果传入的是str直接是文件名
if isinstance(file_name, str):
file_name = "".join([file_name, '.docx'])
file_abs_path = main_download_path / project_path(id) / 'final_seitai' / file_name
if not file_abs_path.is_file():
return ChenResponse(status=404, code=404, message="文档未生成或生成错误!")
response = FileResponse(open(file_abs_path, 'rb'))
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = f"attachment; filename={file_name}.docx"
return response
# 2.如果传入的是列表,多个文件名
elif isinstance(file_name, list):
file_name_list = file_name
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for file_name in file_name_list:
file_name = "".join([file_name, '.docx'])
file_abs_path = main_download_path / project_path(id) / 'final_seitai' / file_name
zip_file.write(file_abs_path, os.path.basename(file_abs_path))
zip_buffer.seek(0)
response = HttpResponse(zip_buffer, content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="回归测试说明文档.zip"'
return response
else:
return ChenResponse(code=500, status=500, message='下载文档出现错误,确认是否有多个轮次内容')

View File

@@ -0,0 +1,31 @@
import logging
from conf.logConfig import LOG_GENERATE_FILE
generate_logger = logging.getLogger("generate_document_logger")
class GenerateLogger(object):
instance = None
# 单例模式
def __new__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = object.__new__(cls)
return cls.instance
else:
return cls.instance
def __init__(self, model: str = '通用文档'):
self.logger = generate_logger
# 模块属性
self.model = model
def write_warning_log(self, fragment: str, message: str):
"""警告日志记录暂时简单点model和message"""
whole_message = f"[{self.model}模块][{fragment}]片段:{message}"
self.logger.warning(whole_message)
@staticmethod
def delete_one_logs():
"""删除生成文档logger的日志记录"""
with open(LOG_GENERATE_FILE, 'w') as f:
f.truncate()

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