initial commit
This commit is contained in:
0
apps/createSeiTaiDocument/__init__.py
Normal file
0
apps/createSeiTaiDocument/__init__.py
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/admin.cpython-313.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/admin.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/admin.cpython-38.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/admin.cpython-38.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/apps.cpython-313.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/apps.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/apps.cpython-38.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/apps.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/controllers.cpython-38.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/controllers.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/docXmlUtils.cpython-38.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/docXmlUtils.cpython-38.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/models.cpython-313.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/models.cpython-38.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/models.cpython-38.pyc
Normal file
Binary file not shown.
BIN
apps/createSeiTaiDocument/__pycache__/schema.cpython-313.pyc
Normal file
BIN
apps/createSeiTaiDocument/__pycache__/schema.cpython-313.pyc
Normal file
Binary file not shown.
3
apps/createSeiTaiDocument/admin.py
Normal file
3
apps/createSeiTaiDocument/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
apps/createSeiTaiDocument/apps.py
Normal file
5
apps/createSeiTaiDocument/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class CreateseitaidocumentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.createSeiTaiDocument'
|
||||
493
apps/createSeiTaiDocument/controllers.py
Normal file
493
apps/createSeiTaiDocument/controllers.py
Normal 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
|
||||
# ~~~~start:2025/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))
|
||||
348
apps/createSeiTaiDocument/docXmlUtils.py
Normal file
348
apps/createSeiTaiDocument/docXmlUtils.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
apps/createSeiTaiDocument/extensions/download_response.py
Normal file
38
apps/createSeiTaiDocument/extensions/download_response.py
Normal 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='下载文档出现错误,确认是否有多个轮次内容')
|
||||
31
apps/createSeiTaiDocument/extensions/logger.py
Normal file
31
apps/createSeiTaiDocument/extensions/logger.py
Normal 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()
|
||||
44
apps/createSeiTaiDocument/extensions/shape_size_tool.py
Normal file
44
apps/createSeiTaiDocument/extensions/shape_size_tool.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from docx.oxml.ns import qn # qn作用是元素的.tag属性,自动帮你处理namespace
|
||||
from docx.shape import InlineShape
|
||||
from apps.createSeiTaiDocument.docXmlUtils import (
|
||||
Demand_table_xqms,
|
||||
Timing_diagram_width,
|
||||
Test_result_width,
|
||||
Horizatal_width
|
||||
)
|
||||
|
||||
def set_shape_size(shape: InlineShape):
|
||||
"""调用下面辅助函数,判断字典{'in_table': True, 'row_idx': 10, 'col_idx': 3}来设置大小"""
|
||||
shape_location = get_shape_location(shape)
|
||||
# 先判断是否在table中
|
||||
if shape_location['in_table']:
|
||||
# 在table中看是否是第一列,第一列则是时序图
|
||||
if shape_location['col_idx'] == 0:
|
||||
# 在第一列:说明是时序图
|
||||
shape.width = Timing_diagram_width
|
||||
else:
|
||||
shape.width = Test_result_width
|
||||
else:
|
||||
shape.width = Horizatal_width
|
||||
|
||||
def get_shape_location(shape: InlineShape):
|
||||
"""传入图片直接处理,注意是python-docx库,不是docxtpl"""
|
||||
# 获取父元素链
|
||||
parent_chain = list(shape._inline.iterancestors())
|
||||
# 检查是否在表格中
|
||||
for elem in parent_chain:
|
||||
if elem.tag == qn("w:tbl"):
|
||||
# 获取表格对象
|
||||
tbl = elem
|
||||
# 获取行对象并计算行索引
|
||||
tr = next(e for e in parent_chain if e.tag == qn('w:tr'))
|
||||
row_idx = tbl.index(tr)
|
||||
# 获取单元格对象并计算列索引
|
||||
tc = next(e for e in parent_chain if e.tag == qn('w:tc'))
|
||||
col_idx = tr.index(tc)
|
||||
return {
|
||||
'in_table': True,
|
||||
'row_idx': row_idx,
|
||||
'col_idx': col_idx
|
||||
}
|
||||
return {'in_table': False}
|
||||
1
apps/createSeiTaiDocument/models.py
Normal file
1
apps/createSeiTaiDocument/models.py
Normal file
@@ -0,0 +1 @@
|
||||
from django.db import models
|
||||
13
apps/createSeiTaiDocument/schema.py
Normal file
13
apps/createSeiTaiDocument/schema.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""定义生成最终文档的BaseModel"""
|
||||
from typing import List
|
||||
from ninja import Schema
|
||||
|
||||
# 定义文档片段输入的Schema,用于输入Schema嵌套
|
||||
class FragmentItemInputSchema(Schema):
|
||||
name: str
|
||||
isCover: bool = True # 默认为需要覆盖生成文档
|
||||
|
||||
# 输入Schema
|
||||
class SeitaiInputSchema(Schema):
|
||||
id: int
|
||||
frag: List[FragmentItemInputSchema]
|
||||
3
apps/createSeiTaiDocument/tests.py
Normal file
3
apps/createSeiTaiDocument/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
apps/createSeiTaiDocument/views.py
Normal file
3
apps/createSeiTaiDocument/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Reference in New Issue
Block a user