Compare commits

11 Commits
v0.1.1 ... main

335 changed files with 4357 additions and 3849 deletions

View File

@@ -19,7 +19,7 @@ from utils.util import get_str_dict, get_list_dict, create_problem_grade_str, cr
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.content_result_tool import create_round_context, create_influence_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
@@ -354,9 +354,11 @@ class GenerateControllerBG(ControllerBase):
# 每个轮次都需要生成一个测试内容和标题
project_path_str = project_path(id)
for round_str in round_str_list:
context = create_round_context(project_obj, round_str)
context, round_obj = create_round_context(project_obj, round_str)
template_path = Path.cwd() / 'media' / project_path_str / 'form_template' / 'bg' / '测试内容和结果_第二轮次.docx'
doc = DocxTemplate(template_path)
# ~~~额外添加:除第一轮次的影响域分析~~~
context['influence'] = create_influence_context(doc, round_obj, project_obj)
doc.render(context, autoescape=True)
try:
doc.save(
@@ -442,7 +444,7 @@ class GenerateControllerBG(ControllerBase):
design_dict['demands'] = '\a'.join(demand_list)
# 通过还是未通过
design_dict['pass'] = '通过'
design_dict['index'] = design_index
design_dict['index'] = design_index # noqa
data_list.append(design_dict)
design_index += 1

View File

@@ -1,13 +1,21 @@
import base64
import io
from typing import Any
from datetime import datetime
from docx.shared import Mm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
from ninja.errors import HttpError
from ninja_extra import ControllerBase, api_controller, route
from django.db import transaction
from django.db.models import Q
from docxtpl import DocxTemplate
from docxtpl import DocxTemplate, InlineImage
from pathlib import Path
from utils.chen_response import ChenResponse
# 导入数据库ORM
from apps.project.models import Project, Contact, Abbreviation
from apps.project.models import Project, Contact, Abbreviation, ProjectSoftSummary, StuctSortData, StaticSoftItem, StaticSoftHardware, \
DynamicSoftTable, DynamicHardwareTable, ProjectDynamicDescription, EvaluateData, EnvAnalysis
from apps.dict.models import Dict
# 导入工具函数
from utils.util import get_str_dict, get_list_dict, get_testType, get_ident, get_str_abbr
@@ -23,7 +31,7 @@ from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
# 导入mixins-处理文档片段
from apps.createDocument.extensions.mixins import FragementToolsMixin
# 导入工具
from apps.createDocument.extensions.tools import demand_sort_by_designKey
from apps.createDocument.extensions.tools import demand_sort_by_designKey, set_table_border_by_cell_position, set_cell_margins
# @api_controller("/generate", tags=['生成大纲文档'], auth=JWTAuth(), permissions=[IsAuthenticated])
@api_controller("/generate", tags=['生成大纲文档'])
@@ -74,12 +82,12 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
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
@@ -102,7 +110,8 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
"test_demand_content": content_list,
"testMethod": testmethod_str.strip(),
"adequacy": single_qs.adequacy.replace("\n", "\a"),
"testDesciption": single_qs.testDesciption.replace("\n", "\a"), # 测试项描述
# 测试项描述FPGA或'静态分析'、'文档审查'、'代码审查'
"testDesciption": single_qs.testDesciption.replace("\n", "\a"),
"testType": get_testType(single_qs.testType, 'testType'),
}
list_list[type_index].append(testdemand_dict)
@@ -282,10 +291,81 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 生成dataSchemas的context - 服务于 1、测评对象 2、动态环境描述
@classmethod
def create_data_schema_list_context(cls, data_qs, doc: DocxTemplate):
if data_qs.exists():
data_list = []
for data_obj in data_qs.all():
item_context: dict[str, Any] = {"fontnote": data_obj.fontnote, 'type': data_obj.type}
# 根据数据类型处理content字段
if data_obj.type == 'text':
item_context['content'] = data_obj.content
elif data_obj.type == 'table':
# 使用subdoc
subdoc = doc.new_subdoc()
rows = len(data_obj.content)
cols = len(data_obj.content[0])
table = subdoc.add_table(rows=rows, cols=cols)
# 单元格处理
for row in range(rows):
for col in range(cols):
cell = table.cell(row, col)
cell.text = data_obj.content[row][col]
# 第一行设置居中
if row == 0:
# 黑体设置
cell.text = ""
pa = cell.paragraphs[0]
run = pa.add_run(str(data_obj.content[row][col]))
run.font.name = '黑体'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
run.font.bold = False
pa.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 垂直居中
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
# 表格居中
table.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 设置边框
set_table_border_by_cell_position(table)
item_context['content'] = subdoc
elif data_obj.type == 'image':
base64_bytes = base64.b64decode(data_obj.content.replace("data:image/png;base64,", ""))
item_context['content'] = InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(120))
data_list.append(item_context)
context = {
"datas": data_list,
}
return context
return None
# 统将需要多个DataSchemas的一对一项目字段生成响应
@classmethod
def uniform_res_from_mul_data_schemas(cls, id: int, filename: str, r_filename: str, model) -> ChenResponse | None:
project_obj = get_object_or_404(Project, id=id)
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / filename
doc = DocxTemplate(input_path)
qs = model.objects.filter(project=project_obj)
if qs.exists():
data_qs = qs.first().data_schemas
context = cls.create_data_schema_list_context(data_qs, doc)
doc.render(context)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / r_filename)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
return None
# 生成测评对象 - 包括大纲、说明、回归说明和报告
@route.get('/create/softComposition', url_name='create-softComposition')
@transaction.atomic
def create_softComposition(self, id: int):
# 首先判断是否包含 - 项目信息-软件概述
res = self.uniform_res_from_mul_data_schemas(id, '测评对象_2.docx', '测评对象.docx', ProjectSoftSummary)
if res is not None:
return res
# 原来文档片段或者初始内容
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, '测评对象')
@@ -303,6 +383,8 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
# 生成被测软件接口章节
@route.get('/create/interface', url_name='create-interface')
def create_interface(self, id: int):
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '被测软件接口.docx'
doc = DocxTemplate(input_path)
project_qs = get_object_or_404(Project, id=id)
project_name = project_qs.name
interfaceNameList = []
@@ -327,12 +409,28 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
'protocal': interface.protocal,
}
interface_list.append(interface_dict)
# 项目接口图处理 - 2026/2/4
image_obj = StuctSortData.objects.filter(project=project_qs)
## 判断是否存在
image_render = None
fontnote = None
if image_obj.exists():
base64_bytes = base64.b64decode(image_obj.first().content.replace("data:image/png;base64,", ""))
image_render = InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(120))
fontnote = image_obj.first().fontnote
context = {
'project_name': project_name,
'iters': interfaceNameList,
'iter_list': interface_list,
'image_render': image_render if image_render else "",
'fontnote': fontnote if fontnote else "".join([project_name, '接口示意图'])
}
return create_dg_docx('被测软件接口.docx', context, id)
doc.render(context, autoescape=True)
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/top_file', url_name='create-performance')
@@ -371,9 +469,87 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
# 通用生成静态软件项、静态硬件项、动态软件项、动态硬件信息的context包含fontnote和table
@classmethod
def create_table_context(cls, table_data: list[list[str]], doc: DocxTemplate):
"""注意:该函数会增加一列序号列"""
subdoc = doc.new_subdoc()
rows = len(table_data)
cols = len(table_data[0]) + 1 # 多渲染一个序号列
table = subdoc.add_table(rows=rows, cols=cols)
# 单元格处理
for row in range(rows):
for col in range(cols):
cell = table.cell(row, col) # 单元格数据
# 设置边距 - 所有单元格
set_cell_margins(cell, left=100, right=100, top=100, bottom=100)
pa = cell.paragraphs[0]
# 处理第一列 - 要居中
if col == 0:
if row == 0:
cell.text = "序号"
else:
cell.text = str(row)
pa.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 处理非第一列
else:
cell.text = table_data[row][col - 1]
# 垂直居中
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
# 单独处理第一行
for col in range(cols):
cell = table.cell(0, col)
cell.text = ""
pa = cell.paragraphs[0]
if col == 0:
run = pa.add_run("序号")
else:
run = pa.add_run(str(table_data[0][col - 1]))
run.font.name = '黑体'
run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
run.font.bold = False
pa.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 设置序号列宽度 - 先自动调整为False然后设置True
for cell in table.columns[0].cells:
cell.width = Mm(15)
pa = cell.paragraphs[0]
pa.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 表格居中
table.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 最后设置表格外边框
set_table_border_by_cell_position(table)
return subdoc
# 统一静态软件项、静态硬件项、动态软件项、动态硬件信息的word生成 - 模版模式
@classmethod
def uniform_static_dynamic_response(cls, id: int, filename: str, r_filename: str, model) -> ChenResponse | None:
project_obj = get_object_or_404(Project, id=id)
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / filename
doc = DocxTemplate(input_path)
qs = model.objects.filter(project=project_obj)
if qs.exists():
obj = qs.first()
table_data = obj.table
subdoc = cls.create_table_context(table_data, doc)
context = {
'fontnote': obj.fontnote,
'table': subdoc,
}
doc.render(context, autoescape=True)
try:
doc.save(Path.cwd() / "media" / project_path(id) / "output_dir" / r_filename)
return ChenResponse(status=200, code=200, message="文档生成成功!")
except PermissionError as e:
return ChenResponse(status=400, code=400, message="模版文件已打开,请关闭后再试,{0}".format(e))
return None
# 静态软件项
@route.get('/create/static_soft', url_name='create-static_soft')
def create_static_soft(self, id: int):
res = self.uniform_static_dynamic_response(id, '静态软件项_2.docx', '静态软件项.docx', StaticSoftItem)
if res is not None:
return res
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, '静态软件项')
@@ -386,6 +562,10 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
# 静态硬件和固件项
@route.get('/create/static_hard', url_name='create-static_hard')
def create_static_hard(self, id: int):
res = self.uniform_static_dynamic_response(id, '静态硬件和固件项_2.docx', '静态硬件和固件项.docx', StaticSoftHardware)
if res is not None:
return res
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, '静态硬件和固件项')
@@ -395,9 +575,14 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
}
return create_dg_docx("静态硬件和固件项.docx", context, id)
# 动态测评环境说明
# 动态测评环境说明 - 多dataSchemas格式
@route.get('/create/dynamic_env', url_name='create-dynamic_env')
def create_dynamic_env(self, id: int):
res = self.uniform_res_from_mul_data_schemas(id, '动态测试环境说明_2.docx',
'动态测试环境说明.docx', ProjectDynamicDescription)
if res is not None:
return res
# 老内容
project_obj: Project = get_object_or_404(Project, id=id)
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '动态测试环境说明.docx'
doc = DocxTemplate(input_path)
@@ -417,6 +602,10 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
# 动态软件项
@route.get('/create/dynamic_soft', url_name='create-dynamic_soft')
def create_dynamic_soft(self, id: int):
res = self.uniform_static_dynamic_response(id, '动态软件项_2.docx', '动态软件项.docx', DynamicSoftTable)
if res is not None:
return res
project_obj: Project = get_object_or_404(Project, id=id)
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '动态软件项.docx'
doc = DocxTemplate(input_path)
@@ -428,9 +617,14 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
}
return create_dg_docx("动态软件项.docx", context, id)
# 动态件项
# 动态件项
@route.get('/create/dynamic_hard', url_name='create-dynamic_hard')
def create_dynamic_hard(self, id: int):
res = self.uniform_static_dynamic_response(id, '动态硬件和固件项_2.docx',
'动态硬件和固件项.docx', DynamicHardwareTable)
if res is not None:
return res
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, '动态硬件和固件项')
@@ -443,6 +637,11 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
# 测试数据
@route.get('/create/test_data', url_name='create-test_data')
def create_test_data(self, id: int):
res = self.uniform_static_dynamic_response(id, '测评数据_2.docx',
'测评数据.docx', EvaluateData)
if res is not None:
return res
# 老内容
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, '测评数据')
@@ -455,6 +654,27 @@ class GenerateControllerDG(ControllerBase, FragementToolsMixin):
# 环境差异性分析
@route.get('/create/env_diff', url_name='create-env_diff')
def create_env_diff(self, id: int):
project_obj: Project = get_object_or_404(Project, id=id)
input_path = Path.cwd() / 'media' / project_path(id) / 'form_template' / 'dg' / '环境差异性分析_2.docx'
doc = DocxTemplate(input_path)
qs = EnvAnalysis.objects.filter(project=project_obj)
if qs.exists():
obj = qs.first()
table_data = obj.table
subdoc = self.create_table_context(table_data, doc)
context = {
"description": obj.description,
"table": subdoc,
"fontnote": obj.fontnote,
}
doc.render(context, autoescape=True)
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))
# 老内容
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, '环境差异性分析')

View File

@@ -8,7 +8,7 @@ from django.db.models import QuerySet, Q
from docxtpl import DocxTemplate
from docx import Document
# 导入模型
from apps.project.models import Project, Round, Dut
from apps.project.models import Project, Round, Dut, InfluenceArea
from apps.dict.models import Dict
# 导入项目工具
from utils.util import get_list_dict, get_str_dict, get_ident, get_case_ident, get_testType
@@ -19,6 +19,8 @@ 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 utils.util import get_str_abbr
from apps.createDocument.extensions.content_result_tool import create_influence_context
# 导入生成日志记录模块
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
# 导入排序
@@ -235,6 +237,9 @@ class GenerateControllerHSM(ControllerBase):
message=f'您第{chinese_round_name[int(hround.key)]}轮次中缺少源代码版本信息,请添加')
last_dm_version = last_round_so_dut.version
now_dm_version = so_dut.version
# 这里插入影响域分析部分并加入context
context_round['influence'] = create_influence_context(doc, hround, project_obj) # noqa
context_round['influence'] = None
# 如果存在这个轮次的需求文档,则查询上个版本
last_xq_version = ""
if xq_dut:

View File

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

View File

@@ -7,7 +7,7 @@ from bs4.element import Tag, NavigableString
import base64
import io
from docxtpl import InlineImage
from docx.shared import Mm, Cm
from docx.shared import Mm
import re
# text.replace('\xa0', ' '))
@@ -22,6 +22,8 @@ class RichParser:
# 最终的解析后的列表
self.data_list = []
self.line_parse()
# 匹配“表1-3”或“表1”等字符的正则
self.biao_pattern = re.compile(r"\d+(?:-\d+)?")
# 1.函数将self.bs.contents去掉\n获取每行数据
def remove_n_in_contents(self):
@@ -82,11 +84,12 @@ class RichParser:
if isinstance(oneline, list):
final_list.append({'isTable': True, 'data': oneline})
continue
if oneline.startswith("data:image/png;base64"):
if oneline.startswith("data:image/png;base64") or oneline.startswith("data:image/jpeg;base64,") or oneline.startswith(
"data:image/jpg;base64,"):
base64_bytes = base64.b64decode(oneline.replace("data:image/png;base64,", ""))
# ~~~设置了固定宽度、高度~~~
final_list.append(
InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(img_size), height=Mm(height)))
inline_image = InlineImage(doc, io.BytesIO(base64_bytes), width=Mm(img_size), height=Mm(height))
final_list.append(inline_image)
else:
final_list.append(oneline)
if len(final_list) <= 0:
@@ -126,6 +129,13 @@ class RichParser:
for oneline in self.data_list:
if isinstance(oneline, list) or oneline.startswith("data:image/png;base64"):
continue
else:
final_list.append(oneline)
cleaned_line = oneline
cleaned_line = re.sub(r'\s+', '', cleaned_line)
cleaned_line = cleaned_line.replace(')', '')
cleaned_line = cleaned_line.strip()
# 去掉以“表3”的行
if self.biao_pattern.search(cleaned_line):
continue
if cleaned_line:
final_list.append(cleaned_line)
return final_list

View File

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

View File

@@ -95,3 +95,4 @@ def delete_dir_files(path: Path) -> Any:
for file in path.iterdir():
if file.is_file():
file.unlink()

View File

@@ -1,3 +1,6 @@
from io import BytesIO
import copy
from docx.parts.image import ImagePart
from pathlib import Path
from django.conf import settings
from django.core.files.storage import FileSystemStorage
@@ -468,22 +471,53 @@ class UploadDocumentController(ControllerBase):
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)
"""将大纲的文档片段储存在/reuse文件夹下面(保留图片)"""
src_doc = Document(self.upload_file)
frag_list = self.get_document_frag_list(src_doc)
for frag_item in frag_list:
# 目的是格式明确按照“测评大纲.docx”进行后续文档一样必须按照这样
if frag_item['content'] is None:
continue
# 1. 创建目标文档(基于 basic_doc.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)
# 2. 逐元素复制 XML并修复图片引用
for child in frag_item['content'].iterchildren():
self._copy_element_with_images(child, src_doc, new_doc)
# 3. 保存片段文件
filename = f"{frag_item['alias']}.docx"
new_doc.save((reuse_dir_path / filename).as_posix())
def _copy_element_with_images(self, element, src_doc, dst_doc):
"""
复制 lxml 元素及其子树,将源文档中的图片关系迁移至目标文档。
"""
# 1. 深拷贝元素
new_element = copy.deepcopy(element)
# 2. 使用本地名称匹配查找图片节点(完全避免命名空间前缀参数)
pic_nodes = new_element.xpath('.//*[local-name()="pic"]')
for pic in pic_nodes:
blip_nodes = pic.xpath('.//*[local-name()="blip"]')
for blip in blip_nodes:
embed_attr = '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed'
old_rId = blip.get(embed_attr)
if not old_rId:
continue
src_image_part = src_doc.part.related_parts.get(old_rId)
if src_image_part is None or not isinstance(src_image_part, ImagePart):
continue
# 添加图片到目标文档,获取新 rId
image_stream = BytesIO(src_image_part.blob)
new_rId, _ = dst_doc.part.get_or_add_image(image_stream)
# 更新属性
blip.set(embed_attr, new_rId)
# 3. 追加到目标文档 body
dst_doc.element.body.append(new_element)
# 辅助函数:将上传文件的文档片段以列表形式返回
def get_document_frag_list(self, doc: Document):
body = doc.element.body

View File

@@ -196,7 +196,11 @@ def generate_temp_doc(doc_type: str, project_id: int, round_num=None, frag_list=
# 根据节点找到图片的关联id
embed = img.xpath('.//a:blip/@r:embed')[0]
# 这里得到ImagePart -> 马上要给新文档添加
related_part: ImagePart = doc_copied.part.related_parts[embed]
related_part: ImagePart = doc_copied.part.related_parts.get(embed)
if related_part is None:
# 可选:记录警告日志,便于排查哪些文档片段有问题
print(f"警告: 文档片段 '{area_pop_name}' 中的图片引用 {embed} 未找到,已跳过!!!!")
continue
# doc_copied.part.related_parts是一个字典
image_part_list.append({'name': area_pop_name, 'img': related_part})

View File

@@ -396,3 +396,21 @@ class CaseController(ControllerBase):
Case.objects.bulk_update(updated_cases, ['exe_time'])
return ChenResponse(status=200, code=200, data=len(updated_cases),
message=f"成功更新{len(updated_cases)}个用例执行时间")
# 给级联选择器数据 -> 上一轮次所有用例
@route.get("/case/getRelatedCase", url_name='case-related-case')
def get_cases_related_case(self, id: int, round_key: str):
project_obj = get_object_or_404(Project, id=id)
previous_round_obj = project_obj.pField.filter(key=int(round_key) - 1).first()
# dut -> design
data_list = []
for dut in previous_round_obj.rdField.all():
dut_dict = {'label': dut.name, 'value': dut.id, 'key': dut.key, 'children': []}
for design in dut.rsField.all():
design_dict = {'label': design.name, 'value': design.id, 'key': design.key, 'children': []}
for case in design.dcField.all():
case_dict = {'label': case.name, 'value': case.id, 'key': case.key}
design_dict['children'].append(case_dict)
dut_dict['children'].append(design_dict)
data_list.append(dut_dict)
return ChenResponse(message='获取成功', data=data_list)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -466,6 +466,9 @@ class Contact(CoreModel):
ordering = ('create_datetime',)
# ~~~~~2024年2月27日新增~~~~~
def default_json_value():
return ""
class Abbreviation(models.Model):
objects = models.Manager()
title = models.CharField(max_length=64, verbose_name="缩略语", help_text="缩略语")
@@ -478,3 +481,163 @@ class Abbreviation(models.Model):
db_table = 'project_abbreviation'
verbose_name = '缩略语和行业词汇'
verbose_name_plural = '缩略语和行业词汇'
# 一对一项目model软件概述
class ProjectSoftSummary(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="projSoftSummary", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
class Meta:
db_table = 'project_soft_summary'
verbose_name = "软件概述表"
verbose_name_plural = verbose_name
# 一对一项目model动态测试环境描述
class ProjectDynamicDescription(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="dynamic_des", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
class Meta:
db_table = 'project_dynamic_description'
verbose_name = "动态环境描述"
verbose_name_plural = verbose_name
# 一对一项目model静态软件项表
class StaticSoftItem(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="static_soft_item", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_static_soft_item'
verbose_name = "静态软件项表"
verbose_name_plural = verbose_name
# 一对一项目model静态硬件项表
class StaticSoftHardware(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="static_hardware", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_static_hardware'
verbose_name = "静态硬件项表"
verbose_name_plural = verbose_name
# 一对一项目model动态软件项表
class DynamicSoftTable(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="dynamic_soft_item", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_dynamic_soft_item'
verbose_name = "动态软件项表"
verbose_name_plural = verbose_name
# 一对一项目model动态硬件项
class DynamicHardwareTable(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="dynamic_hardware", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_dynamic_hardware'
verbose_name = "动态硬件项表"
verbose_name_plural = verbose_name
# 一对一项目model动态环境 - 测评数据
class EvaluateData(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False, related_name="evaluate_data", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
class Meta:
db_table = 'project_evaluate_data'
verbose_name = "测评数据"
verbose_name_plural = verbose_name
# 一对一项目model环境差异性分析
class EnvAnalysis(models.Model):
project = models.OneToOneField(to="Project", primary_key=True, db_constraint=False,
related_name="env_analysis", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
table = models.JSONField(verbose_name="储存表格二维数组", help_text="储存表格二维数组", default=default_json_value)
fontnote = models.CharField(max_length=256, null=True, default="", verbose_name="题注", help_text="数据的题注说明")
description = models.CharField(max_length=1024, null=True, default="", verbose_name="差异性分析文字")
class Meta:
db_table = 'project_env_analysis'
verbose_name = "环境差异性分析表"
verbose_name_plural = verbose_name
# 结构化排序数据
class StuctSortData(CoreModel):
"""
与其他项目信息的多对一关系
"""
# 软件概述内容
soft_summary = models.ForeignKey(ProjectSoftSummary, db_constraint=False, related_name="data_schemas", verbose_name="所属软件概述",
on_delete=models.CASCADE, null=True, blank=True)
# 接口图
project = models.OneToOneField(Project, db_constraint=False, related_name="data_schemas", on_delete=models.CASCADE, null=True, blank=True,
verbose_name="该接口图所属的项目")
# 动态环境描述
dynamic_description = models.ForeignKey(ProjectDynamicDescription, db_constraint=False, related_name="data_schemas",
verbose_name="所属动态环境描述",
on_delete=models.CASCADE, null=True, blank=True)
type = models.CharField(
max_length=20,
choices=(('text', '文本'), ('table', '表格'), ('image', '图片')),
default='text',
verbose_name="数据类型",
)
# 题注字段
fontnote = models.CharField(
max_length=256,
blank=True,
default="",
verbose_name="题注",
help_text="数据的题注说明"
)
# 内容字段 - 存储字符串、列表、字典
content = models.JSONField(verbose_name="内容", help_text="存储文本内容或二维表格数据或图片数据", default=default_json_value)
class Meta:
db_table = 'data_schemas'
verbose_name = "结构排序化数据"
verbose_name_plural = verbose_name
def __str__(self):
return f"结构排序化数据:({self.pk})"
# 影响域分析 - 隶属:轮次(不能是第一轮次)
class InfluenceArea(models.Model):
round = models.OneToOneField(to="Round", primary_key=True, db_constraint=False,
related_name="influence", on_delete=models.CASCADE,
verbose_name="关联项目", help_text="关联项目")
class Meta:
db_table = 'round_influence_area'
verbose_name = "影响域分析"
verbose_name_plural = verbose_name
class InfluenceItem(CoreModel):
# 外键:影响域分析
influence = models.ForeignKey(InfluenceArea, db_constraint=False, related_name="influence_items", verbose_name="所属影响域分析",
on_delete=models.CASCADE, null=True, blank=True)
change_type = models.CharField(max_length=256, null=True, default="", verbose_name="更改类型", help_text="更改类型")
change_influ = models.TextField(max_length=2048, null=True, default="", verbose_name="影响域分析", help_text="影响域分析")
change_des = HTMLField(blank=True, null=True, verbose_name="更改内容描述")
effect_cases = models.JSONField(default=create_list, verbose_name="影响的用例key数组")
class Meta:
db_table = 'influence_item'
verbose_name = "影响域分析 - 行数据"
verbose_name_plural = verbose_name

View File

@@ -1,5 +1,7 @@
from ninja.errors import HttpError
from apps.project.models import Project
from pyasn1_modules.rfc2315 import Data
from apps.project.models import Project, StuctSortData
from ninja import Schema, ModelSchema
from pydantic import field_validator
from typing import List, Optional
@@ -39,3 +41,31 @@ class ProjectCreateInput(ModelSchema):
class DeleteSchema(Schema):
ids: List[int]
# ~~~软件概述~~~
class DataSchema(Schema):
type: Optional[str] = "text"
fontnote: Optional[str] = ""
content: str | list[list[str]]
## 输入
class SoftSummarySchema(Schema):
id: int
data: list[DataSchema]
# ~~~软件接口图~~~
## 复用DataSchema
# ~~~静态软件项、静态硬件项、动态软件项、动态硬件项~~~
class StaticDynamicData(Schema):
id: int
category: str
table: list[list[str]]
fontnote: Optional[str] = ""
# ~~~环境差异性分析~~~
class EnvAnalysisSchema(Schema):
id: int
table: list[list[str]]
fontnote: Optional[str] = ""
description: Optional[str] = ""

View File

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

View File

@@ -182,6 +182,8 @@ def auto_create_wd(user_name: str, dut_qs: Dut, project_obj: Project):
}
new_wd_design_obj: Design = Design.objects.create(**wd_design_create_dict)
# 1.1.1.自动创建demand文档审查
is_JD = (project_obj.report_type == '9')
test_des = "本次三方文档审查内容包括软件需求规格说明、软件设计说明等"
wd_demand_create_dict = {
'ident': 'WDSC',
'name': '文档审查',
@@ -206,7 +208,7 @@ def auto_create_wd(user_name: str, dut_qs: Dut, project_obj: Project):
'13软件研制总结报告\a'
'14软件版本说明\a'
'15软件产品规格说明\a'
'16固件保障手册',
'16固件保障手册' if is_JD else test_des,
'key': ''.join([new_wd_design_obj.key, '-', '0']),
'level': '3',
'project': project_obj,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1422
dbdata.sql Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
[WARNING][2025-04-29 14:20:41,997][logger.py:25][回归测试记录模块][单个问题单表格]片段:问题单4未关联用例请检查
[WARNING][2026-01-20 16:34:34,986][logger.py:25][回归测试说明模块][当前文档全部片段]片段:该项目没有创建轮次

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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