Files
JITToolBox/module/achievement/doc.py

577 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
# #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import io
import os
import traceback
from typing import Callable
from docx import Document
from docx.enum.section import WD_ORIENT
from docx.enum.table import WD_ALIGN_VERTICAL, WD_ROW_HEIGHT_RULE
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Pt, Cm
from docx.text.run import Run
from module import LOGLEVEL
from module.achievement.excel import ExcelReader
class DocxWriter:
full_file_path: str
def __init__(self, save_path: str, filename: str, excel_reader: ExcelReader,
signal: Callable[[str, str], None] = lambda x, y: print(x)):
super().__init__()
self.save_path = save_path
self.filename = filename
self.excel_reader = excel_reader
self.full_file_path = os.path.join(save_path, filename) + '.docx'
self.signal = signal
def write(self) -> int:
try:
if not os.path.exists(self.save_path):
os.makedirs(self.save_path)
if os.path.exists(self.full_file_path):
self.signal(f"文件'{self.filename}'已存在,将覆盖原文件", LOGLEVEL.WARNING)
doc = Document()
for section in doc.sections:
new_width, new_height = Cm(29.7), Cm(21) # A4纸张
# 设置纸张方向为横向
section.orientation = WD_ORIENT.LANDSCAPE
# 设置页边距例如上下左右均为2厘米
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
section.left_margin = Cm(2)
section.right_margin = Cm(1)
section.page_width = new_width
section.page_height = new_height
paragraph = doc.add_paragraph()
# 设置段落居中对齐
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run(f"{self.excel_reader.course_name}》课程目标达成情况评价报告")
self.set_run_font(run, 16, '黑体', '黑体', True)
paragraph = doc.add_paragraph()
run = paragraph.add_run(
f"1.班级{'(年级)' if len(self.excel_reader.class_list) == 1 else ''}课程目标达成情况分析")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
# 班级课程目标达成情况分析表
class_info_str = "班级:{} 学生人数:{} 任课教师:{}"
for r_index in range(len(self.excel_reader.class_list)):
self.signal(f"正在生成 {self.excel_reader.class_list[r_index]} 表格", LOGLEVEL.INFO)
paragraph = doc.add_paragraph()
# 设置段落为分散对齐
paragraph.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE
run = paragraph.add_run(class_info_str.format(
self.excel_reader.class_list[r_index],
self.excel_reader.class_number[r_index],
self.excel_reader.course_teacher_name
))
self.set_run_font(run, 10.5, 'Times New Roman', '宋体')
if len(self.excel_reader.class_list) == 1:
rows = 14 + self.excel_reader.class_number[r_index] + self.excel_reader.kpi_number
else:
rows = 12 + self.excel_reader.class_number[r_index] + self.excel_reader.kpi_number
cols = 2 + self.calculate_columns(self.excel_reader.achievement_level[r_index]) # 加上第一列
table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table)
# 合并前六行头两个单元格
for row in range(6):
cell_start = table.cell(row, 0)
cell_end = table.cell(row, 1)
cell_start.merge(cell_end)
# 合并前三行的单元格
for row in range(3):
col_span = 2 # 从第三列开始
for performance in self.excel_reader.achievement_level[r_index]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
X = self.excel_reader.kpi_number + 5 # X的值
if len(self.excel_reader.class_list) == 1:
X += 2
# 第一、二行合并第二个单元格
for row in range(rows - X, rows - X + 2):
table.cell(row, 0).merge(table.cell(row, 1))
# 第三行的合并操作
# 第一个单元格与第三+len(a[0])-1行第二个单元格合并
table.cell(rows - X + 2, 0).merge(table.cell(rows - X + 2 + self.excel_reader.kpi_number - 1, 1))
for i in range(self.excel_reader.kpi_number):
# 第三个与第四个单元格合并
table.cell(rows - X + 2 + i, 2).merge(table.cell(rows - X + i + 2, 3))
# 从第五个单元格合并到最后
start_cell = table.cell(rows - X + 2 + i, 4)
end_cell = table.cell(rows - X + 2 + i, cols - 1)
start_cell.merge(end_cell)
# 第三+len(a[0])行的合并操作
# 合并第一个到第二个单元格
table.cell(rows - X + 2 + self.excel_reader.kpi_number, 0).merge(
table.cell(rows - X + 2 + self.excel_reader.kpi_number, 1))
# 合并第三个到最后一个单元格
start_cell = table.cell(rows - X + 2 + self.excel_reader.kpi_number, 2)
end_cell = table.cell(rows - X + 2 + self.excel_reader.kpi_number, cols - 1)
start_cell.merge(end_cell)
if len(self.excel_reader.class_list) == 1:
# 合并前两个单元格
for i in range(2):
start = rows - X + 3 + i + self.excel_reader.kpi_number
table.cell(start, 0).merge(table.cell(start, 1))
# 依次合并后面的单元格
for row in range(start, start + self.excel_reader.kpi_number):
col_span = 2 # 从第三列开始
for performance in self.excel_reader.achievement_level[r_index]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
start = rows - X + 3 + self.excel_reader.kpi_number
if len(self.excel_reader.class_list) == 1:
start += 2
end = rows
for row in range(start, end):
start_cell = table.cell(row, 1)
end_cell = table.cell(row, cols - 1)
start_cell.merge(end_cell)
# 填充表格数据
self.put_data_to_table(table, self.excel_reader.get_word_template, r_index)
doc.add_page_break()
if len(self.excel_reader.class_list) > 1:
paragraph = doc.add_paragraph()
run = paragraph.add_run("2.年级课程目标达成情况分析")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
# 2.年级课程目标达成情况分析
class_info_str = "班级:{} 课程负责人:{}"
paragraph = doc.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE
run = paragraph.add_run(class_info_str.format(
self.excel_reader.total_class_str,
self.excel_reader.course_lead_teacher_name
))
self.set_run_font(run, 10.5, "Times New Roman", '宋体')
rows = 10 + len(self.excel_reader.class_list)
cols = 2 + self.calculate_columns(self.excel_reader.achievement_level[0]) # 加上第一列
table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table)
# 合并前六行头两个单元格
for row in range(rows - 2):
cell_start = table.cell(row, 0)
cell_end = table.cell(row, 1)
cell_start.merge(cell_end)
# 合并前三行的单元格
for row in range(3):
col_span = 2 # 从第三列开始
for performance in self.excel_reader.achievement_level[0]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
# 合并中间X行的单元格
for row in range(6, 8 + len(self.excel_reader.class_list)):
col_span = 2
for performance in self.excel_reader.achievement_level[0]:
non_none_count = 3 - performance.scores.count(None)
if non_none_count > 1:
cell_start = table.cell(row, col_span)
cell_end = table.cell(row, col_span + non_none_count - 1)
cell_start.merge(cell_end)
col_span += non_none_count
# 合并最后两行的单元格
for row in range(2):
cell_start = table.cell(rows - row - 1, 1)
cell_end = table.cell(rows - row - 1, cols - 1)
cell_start.merge(cell_end)
# 填充数据
self.put_data_to_table(table, self.excel_reader.get_word_template_part_2)
doc.add_page_break()
# 3. 课程目标达成情况的合理性评价
paragraph = doc.add_paragraph()
run = paragraph.add_run(f"{3 if len(self.excel_reader.class_list) > 1 else 2}"
f". 课程目标达成情况的合理性评价")
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
rows = 11
cols = 6
table = doc.add_table(rows=rows, cols=cols)
# 设置外侧框线粗1.5磅内侧框线粗0.5磅
self.set_table_borders(table)
# 合并第一行
cell_start = table.cell(0, 0)
cell_end = table.cell(0, cols - 1)
cell_start.merge(cell_end)
# 合并第二行至最后
for i in range(1, rows):
match i:
case 2:
table.cell(i, 2).merge(table.cell(i, 3))
table.cell(i, 4).merge(table.cell(i, 5))
table.cell(i, 1).width = Cm(7.42)
table.cell(i, 2).width = Cm(7.42)
table.cell(i, 4).width = Cm(7.41)
case 8 | 10:
table.cell(i - 1, 0).merge(table.cell(i, 0))
table.cell(i, 1).width = Cm(11.23)
table.cell(i, 2).width = Cm(1.48)
table.cell(i, 3).width = Cm(3.4)
table.cell(i, 4).width = Cm(1.39)
case _:
cell_start = table.cell(i, 1)
cell_end = table.cell(i, cols - 1)
cell_start.merge(cell_end)
# 填充数据
self.put_data_to_table(table, self.excel_reader.get_word_template_part_3)
# 应用样式
self.signal("正在应用文字样式", LOGLEVEL.INFO)
# 遍历文档中的所有段落
for index, paragraph in enumerate(doc.paragraphs):
if index == 0:
self.set_paragraph_format(paragraph, WD_LINE_SPACING.ONE_POINT_FIVE)
else:
self.set_paragraph_format(paragraph)
# 遍历文档中的所有表格及其单元格
t_len = len(doc.tables)
part_1_table_index = [x for x in range(len(self.excel_reader.class_list))]
part_2_table_index = [len(part_1_table_index)] if len(self.excel_reader.class_list) != 1 else []
part_3_table_index = [t_len - 1]
for t_index, table in enumerate(doc.tables):
self.set_table_borders(table)
# part_3_table_index 表格第9和11行索引8和10特殊边框处理
if t_index in part_3_table_index:
for r_idx in [8, 10]:
row = table.rows[r_idx]
prev_row = table.rows[r_idx - 1]
# 上一行第8行和第10行索引7和9第2-6列移除下边框
for c_idx in range(1, 6):
self.set_cell_border(prev_row.cells[c_idx], bottom=0)
# 第2列索引1没有上边框和右边框
self.set_cell_border(row.cells[1], top=0, right=0)
# 第3-5列索引2-4没有上边框和左右边框
for c_idx in [2, 3, 4]:
self.set_cell_border(row.cells[c_idx], top=0, left=0, right=0)
# 第6列索引5没有上边框和左边框
self.set_cell_border(row.cells[5], top=0, left=0)
# 插入签名图片
if self.excel_reader.major_director_signature_image is not None:
self.insert_pil_image(table.cell(8, 3),
self.excel_reader.major_director_signature_image,
height=Cm(1.2))
if self.excel_reader.course_leader_signature_image is not None:
self.insert_pil_image(table.cell(10, 3),
self.excel_reader.course_leader_signature_image,
height=Cm(1.2))
for r_index, row in enumerate(table.rows):
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
# part_3_table_index 表格第9和11行索引8和10行高为1.2cm
if t_index in part_3_table_index and r_index in [8, 10]:
row.height = Cm(1.2)
else:
row.height = Cm(0.7)
for c_index, cell in enumerate(row.cells):
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
self.set_cell_margins(cell, start=57, end=57)
for paragraph in cell.paragraphs:
for run in paragraph.runs:
self.set_run_font(run, 10.5, 'Times New Roman', '宋体')
self.set_paragraph_format(paragraph)
special_cell = []
if t_index in part_1_table_index:
ll = [(9 + x + self.excel_reader.class_number[t_index], 4) for x in
range(self.excel_reader.kpi_number)]
start_line = 9 + self.excel_reader.kpi_number
special_cell.extend(ll)
special_cell.extend([
(1, 2),
(2, 2),
(start_line + self.excel_reader.class_number[t_index], 2),
])
if len(self.excel_reader.class_list) == 1:
special_cell.extend([
(start_line + 3 + self.excel_reader.class_number[t_index], 1),
(start_line + 4 + self.excel_reader.class_number[t_index], 1),
])
lst = [(start_line + 3 + self.excel_reader.class_number[t_index], 1)]
else:
special_cell.extend([
(start_line + 1 + self.excel_reader.class_number[t_index], 1),
(start_line + 2 + self.excel_reader.class_number[t_index], 1),
])
lst = [(start_line + 1 + self.excel_reader.class_number[t_index], 1)]
if (r_index, c_index) in lst:
self.insert_image_from_data(cell,
self.excel_reader.pic_list[t_index],
Cm(7 * self.excel_reader.kpi_number)
)
if (len(self.excel_reader.class_list) == 1 and
r_index == start_line + 2 + self.excel_reader.class_number[t_index]):
self.change_font_to_wingding2(paragraph)
elif t_index in part_2_table_index:
special_cell = [
(1, 2),
(2, 2),
(8 + len(self.excel_reader.class_list), 1),
(9 + len(self.excel_reader.class_list), 1),
]
if r_index == 7 + len(self.excel_reader.class_list):
self.change_font_to_wingding2(paragraph)
elif t_index in part_3_table_index:
special_cell = [
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 1),
(6, 1),
(7, 1),
(8, 1),
(9, 1),
(10, 1),
]
if r_index == 0:
for run in paragraph.runs:
self.set_run_font(run, 10.5, 'Times New Roman', '宋体', True)
else:
self.change_font_to_wingding2(paragraph)
if any(r_index == pair[0] and c_index >= pair[1] for pair in special_cell):
paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
else:
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.save(self.full_file_path)
except PermissionError as e:
raise Exception(f"""
简要错误信息:{str(e)}
这可能是由于Word文件被占用所导致的请关闭相应文件后再试。
""")
except Exception as _:
error_message = traceback.format_exc()
raise Exception(f"""
原始错误信息:
{error_message}
""")
def set_save_path(self, save_path: str):
self.save_path = save_path
self.full_file_path = os.path.join(save_path, self.filename)
def set_filename(self, filename: str):
self.filename = filename
self.full_file_path = os.path.join(self.save_path, filename)
def set_run_font(self, run: Run, size, western_font, chinese_font, bold=False):
run.font.size = Pt(size)
run.font.name = western_font
run.bold = bold
run._element.rPr.rFonts.set(qn('w:eastAsia'), chinese_font)
def set_cell_border(self, cell, **kwargs):
"""
设置单元格边框
kwargs: top, bottom, left, right, inside_h, inside_v
值为0时移除边框值大于0时设置边框粗细
"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
# 创建一个新的'TC边框'元素
tcBorders = OxmlElement('w:tcBorders')
# 对于每个边框方向
for key, value in kwargs.items():
if value is not None:
tag = 'w:{}'.format(key)
border = OxmlElement(tag)
if value == 0:
# 移除边框
border.set(qn('w:val'), 'nil')
else:
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), str(int(value * 8)))
border.set(qn('w:space'), '0')
border.set(qn('w:color'), 'auto')
tcBorders.append(border)
# 将边框添加到单元格属性中
tcPr.append(tcBorders)
def set_paragraph_format(self, paragraph, line_spacing_rule: WD_LINE_SPACING = WD_LINE_SPACING.SINGLE):
paragraph_format = paragraph.paragraph_format
paragraph_format.line_spacing_rule = line_spacing_rule # 单倍行距
paragraph_format.space_before = Pt(0) # 段前间距
paragraph_format.space_after = Pt(0) # 段后间距
def calculate_columns(self, performance_list):
"""
计算表格的列数
:param performance_list:
:return:
"""
col_count = 0
for performance in performance_list:
col_count += 3 - performance.scores.count(None) # 非None值的数量
return col_count
def put_data_to_table(self, table, gen_func, *args):
"""填充表格数据"""
data = gen_func(*args)
for i, row in enumerate(table.rows):
for j, cell in enumerate(row.cells):
if cell.text == "":
# 填充数据
# 这里需要根据您的数据结构来调整
d = next(data)
cell.text = str(d)
def set_cell_margins(self, cell, **kwargs):
"""
设置单元格的边距。
cell: 要修改的单元格实例
kwargs: 边距值以twips为单位1/1440英寸
示例set_cell_margins(cell, top=50, start=50, bottom=50, end=50)
"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcMar = OxmlElement('w:tcMar')
for m in ["top", "start", "bottom", "end"]:
if m in kwargs:
node = OxmlElement("w:{}".format(m))
node.set(qn('w:w'), str(kwargs.get(m)))
node.set(qn('w:type'), 'dxa')
tcMar.append(node)
tcPr.append(tcMar)
def is_start_of_merged_cells(self, cell):
""" 检查单元格是否是合并单元格的起始单元格 """
merge = cell._element.xpath('./w:tcPr/w:vMerge')
if merge:
return merge[0].get(qn('w:val')) == 'restart'
return False
def is_merged_vertically(self, cell):
""" 检查单元格是否垂直合并 """
merge = cell._element.xpath('./w:tcPr/w:vMerge')
return bool(merge)
def set_table_borders(self, table, outside_sz=1.5, inside_sz=0.5):
"""
设置整个表格的边框样式
outside_sz: 外侧边框大小
inside_sz: 内部分割线大小
"""
for row in table.rows:
for cell in row.cells:
self.set_cell_border(cell, top=inside_sz, bottom=inside_sz, left=inside_sz, right=inside_sz)
# 设置外侧边框
for cell in table.rows[0].cells:
self.set_cell_border(cell, top=outside_sz)
for cell in table.rows[-1].cells:
self.set_cell_border(cell, bottom=outside_sz)
for row in table.rows:
self.set_cell_border(row.cells[0], left=outside_sz)
self.set_cell_border(row.cells[-1], right=outside_sz)
def insert_image_from_data(self, cell, image_data, width=Cm(21), height=Cm(4.5)):
# 创建一个临时文件
with io.BytesIO(image_data) as image_stream:
image_stream.seek(0)
# 插入图片
# 在单元格中添加一个段落
paragraph = cell.paragraphs[0]
# 在段落中插入图片
run = paragraph.add_run()
run.add_picture(image_stream, width=width, height=height)
def insert_pil_image(self, cell, pil_image, width=None, height=Cm(4.5)):
"""插入PIL Image对象到单元格"""
image_stream = io.BytesIO()
pil_image.save(image_stream, format='PNG')
image_stream.seek(0)
paragraph = cell.paragraphs[0]
run = paragraph.add_run()
run.add_picture(image_stream, width=width, height=height)
def is_chinese(self, char):
"""判断字符是否为中文"""
if '\u4e00' <= char <= '\u9fff':
return True
return False
def change_font_to_wingding2(self, paragraph):
for run in paragraph.runs:
if not run.text.strip(): # 跳过不包含文本的runs
continue
new_text = ""
for char in run.text:
if char in ['R', '£']:
# 对于特殊字符创建新的run
if new_text:
new_run = paragraph.add_run(new_text)
self.set_run_font(new_run, 10.5, 'Times New Roman', '宋体')
new_text = ""
special_run = paragraph.add_run(char)
self.set_run_font(special_run, 10.5, 'Wingdings 2', '宋体')
else:
# 累积其他字符
new_text += char
if new_text:
# 创建剩余文本的run
new_run = paragraph.add_run(new_text)
self.set_run_font(new_run, 10.5, 'Times New Roman', '宋体')
run.clear()