合并达成度功能
This commit is contained in:
BIN
images/3rd/qfluentwidgets.png
Normal file
BIN
images/3rd/qfluentwidgets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
BIN
images/logo.png
Normal file
BIN
images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 432 KiB |
1
main.py
1
main.py
@@ -3,6 +3,7 @@ import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from ui.main import MainWindow
|
||||
import module.resources
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -20,7 +20,7 @@ splash = Splash(
|
||||
'images\\splash.png',
|
||||
binaries=a.binaries,
|
||||
datas=a.datas,
|
||||
text_pos=(5,298),
|
||||
text_pos=(5,378),
|
||||
text_size=12,
|
||||
text_color='black',
|
||||
minify_script=True,
|
||||
@@ -47,5 +47,6 @@ exe = EXE(
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
name='答辩题目生成器',
|
||||
name='建工工具箱',
|
||||
icon=['images\\logo.png'],
|
||||
)
|
||||
|
||||
22
module/__init__.py
Normal file
22
module/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
VER_NUM = '1.3.14'
|
||||
VERSION = f'Release {VER_NUM}'
|
||||
COMPATIBLE_VERSION = ['7.3', '7.4', '7.6', '7.7', '8.0']
|
||||
|
||||
|
||||
class CONSOLE:
|
||||
FONT_FAMILY = ("'JetBrains Mono', 'Consolas', 'Menlo', 'Monaco', 'Courier New', "
|
||||
"'Ubuntu Mono', 'DejaVu Sans Mono', 'Liberation Mono', monospace")
|
||||
BACKGROUND = "#282A36"
|
||||
RED = "#FF5555"
|
||||
YELLOW = "#F1FA8C"
|
||||
WHITE = "#DADBD7"
|
||||
GREEN = "#50FA7B"
|
||||
BLACK = "#63656C"
|
||||
LINE_HEIGHT = "1.2"
|
||||
|
||||
|
||||
class LOGLEVEL:
|
||||
INFO = "INFO"
|
||||
WARNING = "WARNING"
|
||||
ERROR = "ERROR"
|
||||
SUCCESS = "SUCCESS"
|
||||
505
module/achievement_doc.py
Normal file
505
module/achievement_doc.py
Normal file
@@ -0,0 +1,505 @@
|
||||
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 = 9
|
||||
cols = 4
|
||||
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, 9):
|
||||
if i == 2:
|
||||
continue
|
||||
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)
|
||||
for r_index, row in enumerate(table.rows):
|
||||
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
|
||||
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),
|
||||
]
|
||||
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
|
||||
"""
|
||||
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)
|
||||
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 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()
|
||||
589
module/achievement_excel.py
Normal file
589
module/achievement_excel.py
Normal file
@@ -0,0 +1,589 @@
|
||||
import datetime
|
||||
import traceback
|
||||
from typing import Optional, Callable
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.utils import get_column_letter, column_index_from_string
|
||||
from openpyxl.workbook.workbook import Workbook
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from packaging import version
|
||||
|
||||
from module import LOGLEVEL, COMPATIBLE_VERSION
|
||||
from module.schema import Performance
|
||||
from utils.function import format_ranges, get_rank, min_score_people_name, get_class_index_range, check_version, \
|
||||
gen_picture
|
||||
|
||||
|
||||
class ExcelReader:
|
||||
kpi_list: list[str]
|
||||
hml_list: list[str]
|
||||
kpi_number: int
|
||||
file_path: str
|
||||
course_name: str
|
||||
course_teacher_name: str
|
||||
total_class_str: str
|
||||
class_list: list[str]
|
||||
class_number: list[int]
|
||||
course_objectives: list[str]
|
||||
course_objectives_number: list[int]
|
||||
evaluation_stage: list[tuple[Optional[str], Optional[str]]]
|
||||
achievement_level: list[list[Performance]]
|
||||
target_score: list[list[float]]
|
||||
n_evaluation_methods: list[Optional[str]]
|
||||
question_data: dict[str, list[tuple[str, int]]]
|
||||
ignore_version_check: bool
|
||||
pic_list: list
|
||||
|
||||
def __init__(self, file_path: str, version_check: bool = False,
|
||||
signal: Callable[[str, str], None] = lambda x, y: print(x)):
|
||||
super().__init__()
|
||||
self.file_path = file_path
|
||||
self.kpi_list = []
|
||||
self.class_list = []
|
||||
self.class_number = []
|
||||
self.course_objectives = []
|
||||
self.course_objectives_number = []
|
||||
self.course_name = ""
|
||||
self.course_teacher_name = ""
|
||||
self.course_lead_teacher_name = ""
|
||||
self.evaluation_stage = []
|
||||
self.achievement_level = []
|
||||
self.kpi_number = 0
|
||||
self.stu_kpi_achieve_list = []
|
||||
self.target_score = []
|
||||
self.n_evaluation_methods = []
|
||||
self.total_class_str = ""
|
||||
self.hml_list = []
|
||||
self.question_data = {}
|
||||
self.ignore_version_check = version_check
|
||||
self.pic_list = []
|
||||
self.signal = signal
|
||||
|
||||
def parse_excel(self):
|
||||
try:
|
||||
wb: Workbook = openpyxl.load_workbook(self.file_path, read_only=True, data_only=True)
|
||||
sheet: Worksheet = wb['初始录入']
|
||||
# 读取版本号
|
||||
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
|
||||
if e_version is None:
|
||||
e_version = "0"
|
||||
status, _ = check_version(e_version, COMPATIBLE_VERSION)
|
||||
CUR_VERSION = version.parse(e_version)
|
||||
if not status:
|
||||
if not self.ignore_version_check:
|
||||
raise NotImplementedError(f"版本不适配,当前表格版本:{e_version},适配版本:{COMPATIBLE_VERSION}")
|
||||
self.signal("已忽略表格兼容性:您的表格版本不在适配版本中,可能导致未知错误。", LOGLEVEL.WARNING)
|
||||
# 读取教学班级
|
||||
self.total_class_str = sheet["D10"].value
|
||||
# 读取课程名称
|
||||
match CUR_VERSION:
|
||||
case _ if CUR_VERSION <= version.parse("7.3"):
|
||||
self.course_name = sheet["D6"].value
|
||||
case _ if CUR_VERSION >= version.parse("7.4"):
|
||||
self.course_name = sheet["D5"].value
|
||||
# 读取任课教师
|
||||
self.course_teacher_name = sheet["D7"].value
|
||||
# 读取课程负责人
|
||||
self.course_lead_teacher_name = sheet["D8"].value
|
||||
|
||||
# 读取班级和人数
|
||||
max_class_size = 4
|
||||
match CUR_VERSION:
|
||||
case _ if CUR_VERSION <= version.parse("7.6"):
|
||||
max_class_size = 3
|
||||
|
||||
for i in range(2, 2 + max_class_size):
|
||||
name = sheet[f"K{i}"]
|
||||
number = sheet[f"M{i}"]
|
||||
if name.value is None or number.value is None:
|
||||
break
|
||||
self.class_list.append(name.value)
|
||||
self.class_number.append(int(number.value))
|
||||
# 读取课程指标个数
|
||||
self.kpi_number = sheet["H8"].value
|
||||
# 读取课程目标和指标点
|
||||
hml_start = 22
|
||||
match CUR_VERSION:
|
||||
case _ if CUR_VERSION <= version.parse("7.6"):
|
||||
hml_start = 21
|
||||
for i in range(hml_start, hml_start + 5):
|
||||
hml = sheet[f"H{i}"]
|
||||
objective = sheet[f"I{i}"]
|
||||
kpi = sheet[f"Q{i}"]
|
||||
if kpi.value is None or objective.value is None:
|
||||
break
|
||||
self.hml_list.append(hml.value)
|
||||
self.kpi_list.append(kpi.value)
|
||||
self.course_objectives.append(objective.value)
|
||||
# 读取考核方式及权重
|
||||
k_start = 7
|
||||
match CUR_VERSION:
|
||||
case _ if CUR_VERSION <= version.parse("7.6"):
|
||||
k_start = 6
|
||||
for i in range(k_start, k_start + 6):
|
||||
if i in (k_start + 1, k_start + 2, k_start + 4):
|
||||
continue
|
||||
way = sheet[f"K{i}"]
|
||||
per = sheet[f"L{i}"]
|
||||
self.evaluation_stage.append((way.value, per.value))
|
||||
|
||||
# 读取平时考核方式
|
||||
end = k_start + 5
|
||||
if CUR_VERSION >= version.parse("7.7"):
|
||||
end += 1
|
||||
for i in range(k_start, end):
|
||||
val = sheet[f"M{i}"].value
|
||||
self.n_evaluation_methods.append(val)
|
||||
|
||||
sheet = wb['达成度']
|
||||
# 目标值表格列 | 目标X |
|
||||
kpi_row_tuple = ( # | 平时 | 大作业 | 试卷 | 个人达成值 |
|
||||
('D', 'E', 'F', 'G'), # 目标一 平均分 | |
|
||||
('H', 'I', 'J', 'K'), # 目标二 得分率 | |
|
||||
('L', 'M', 'N', 'O'), # 目标三 达成度 | |
|
||||
('P', 'Q', 'R', 'S'), # 目标四
|
||||
('T', 'U', 'V', 'W') # 目标五
|
||||
)
|
||||
# 获取达成度中的目标分值、班级平均分,得分率和达成度
|
||||
for i in range(self.kpi_number):
|
||||
# 目标分值
|
||||
self.target_score.append([
|
||||
sheet[kpi_row_tuple[i][0] + f"6"].value,
|
||||
sheet[kpi_row_tuple[i][1] + f"6"].value,
|
||||
sheet[kpi_row_tuple[i][2] + f"6"].value
|
||||
])
|
||||
base_row = 8
|
||||
for j in range(len(self.class_number)):
|
||||
# 平均分
|
||||
avg = (
|
||||
sheet[kpi_row_tuple[i][0] + f"{base_row + j * 3}"].value,
|
||||
sheet[kpi_row_tuple[i][1] + f"{base_row + j * 3}"].value,
|
||||
sheet[kpi_row_tuple[i][2] + f"{base_row + j * 3}"].value
|
||||
)
|
||||
# 得分率
|
||||
per = (
|
||||
sheet[kpi_row_tuple[i][0] + f"{base_row + 1 + j * 3}"].value,
|
||||
sheet[kpi_row_tuple[i][1] + f"{base_row + 1 + j * 3}"].value,
|
||||
sheet[kpi_row_tuple[i][2] + f"{base_row + 1 + j * 3}"].value
|
||||
)
|
||||
# 达成度
|
||||
kpi_c = sheet[kpi_row_tuple[i][0] + f"{base_row + 2 + j * 3}"].value
|
||||
if len(self.achievement_level) - 1 < j:
|
||||
self.achievement_level.append([Performance(avg, per, kpi_c)])
|
||||
else:
|
||||
self.achievement_level[j].append(Performance(avg, per, kpi_c))
|
||||
# 获取学生的各项目标值
|
||||
# 读取 A18:O87 范围的数据
|
||||
match CUR_VERSION:
|
||||
case _ if CUR_VERSION <= version.parse("7.6"):
|
||||
rows = sheet[f'B18:{kpi_row_tuple[self.kpi_number - 1][3]}{17 + sum(self.class_number)}']
|
||||
case _:
|
||||
rows = sheet[f'B21:{kpi_row_tuple[self.kpi_number - 1][3]}{20 + sum(self.class_number)}']
|
||||
|
||||
# 将数据按行保存到列表中
|
||||
for row in rows:
|
||||
row_data = [cell.value for cell in row]
|
||||
self.stu_kpi_achieve_list.append(row_data)
|
||||
|
||||
# 获取试题信息
|
||||
sheet = wb['成绩录入']
|
||||
start_col = 'BC' # 起始列
|
||||
row_keys = 9 # 键所在的行
|
||||
row_values = [7, 8] # 值所在的行
|
||||
|
||||
# 确定终止列
|
||||
col = start_col
|
||||
while True:
|
||||
if not sheet[col + str(row_keys)].value:
|
||||
break
|
||||
col = get_column_letter(column_index_from_string(col) + 1)
|
||||
|
||||
# 读取数据
|
||||
for col_index in range(column_index_from_string(start_col),
|
||||
column_index_from_string(col)):
|
||||
col_letter = get_column_letter(col_index)
|
||||
key = sheet[col_letter + str(row_keys)].value
|
||||
if key: # 确保键不为空
|
||||
values = tuple(sheet[col_letter + str(r)].value for r in row_values)
|
||||
if key in self.question_data:
|
||||
self.question_data[key].append(values)
|
||||
else:
|
||||
self.question_data[key] = [values]
|
||||
|
||||
self.validate_data()
|
||||
self.gen_picture()
|
||||
|
||||
except Exception as e:
|
||||
error_message = traceback.format_exc()
|
||||
raise f"""
|
||||
原始错误:
|
||||
{error_message}
|
||||
"""
|
||||
|
||||
def set_file_path(self, file_path: str):
|
||||
self.file_path = file_path
|
||||
|
||||
def validate_data(self):
|
||||
self.signal("正在验证数据", LOGLEVEL.INFO)
|
||||
return 0
|
||||
|
||||
def run(self):
|
||||
self.parse_excel()
|
||||
|
||||
def clear_all_data(self):
|
||||
self.kpi_list = []
|
||||
self.kpi_number = 0
|
||||
self.file_path = ""
|
||||
self.course_name = ""
|
||||
self.course_teacher_name = []
|
||||
self.class_list = []
|
||||
self.class_number = []
|
||||
self.course_objectives = []
|
||||
self.course_objectives_number = []
|
||||
self.evaluation_stage = []
|
||||
self.achievement_level = []
|
||||
self.kpi_number = 0
|
||||
self.stu_kpi_achieve_list = []
|
||||
self.target_score = []
|
||||
self.n_evaluation_methods = []
|
||||
self.hml_list = []
|
||||
self.question_data = {}
|
||||
self.pic_list = []
|
||||
|
||||
def set_version_check(self, version_check: bool):
|
||||
self.ignore_version_check = version_check
|
||||
|
||||
def gen_picture(self):
|
||||
self.signal("正在生成散点图", LOGLEVEL.INFO)
|
||||
for i in range(len(self.class_list)):
|
||||
l_index, r_index = get_class_index_range(self.class_number, i)
|
||||
self.pic_list.append(gen_picture(
|
||||
self.stu_kpi_achieve_list,
|
||||
self.kpi_number,
|
||||
l_index,
|
||||
r_index
|
||||
))
|
||||
|
||||
def get_word_template(self, class_index):
|
||||
yield "课程目标"
|
||||
for i in range(self.kpi_number):
|
||||
yield f"课程目标{i + 1}({self.hml_list[i]})"
|
||||
yield ("总成绩=\n{}".
|
||||
format(
|
||||
"\n+".join([f"{n if n != '试卷' else '期末'}成绩×{int(p * 100)}%" for n, p in self.evaluation_stage if
|
||||
p is not None])))
|
||||
for i in self.course_objectives:
|
||||
yield i
|
||||
yield "支撑毕业要求指标点"
|
||||
for i in self.kpi_list:
|
||||
yield i
|
||||
yield "考核类型"
|
||||
for i in range(self.kpi_number):
|
||||
s_lst = self.achievement_level[class_index][i].scores
|
||||
for index, (j, s) in enumerate(zip(self.evaluation_stage, s_lst)):
|
||||
if s is None:
|
||||
continue
|
||||
if index == 2:
|
||||
if len(self.n_evaluation_methods) == 6:
|
||||
yield f"期末考核\n({self.n_evaluation_methods[5]})"
|
||||
else:
|
||||
yield f"期末考核\n({j[0]})"
|
||||
else:
|
||||
yield f"{j[0]}考核"
|
||||
|
||||
yield "目标分值(分)"
|
||||
for i in self.target_score:
|
||||
for j in i:
|
||||
if j is None:
|
||||
continue
|
||||
yield j
|
||||
yield "权重"
|
||||
for i in range(self.kpi_number):
|
||||
s_lst = self.achievement_level[class_index][i].scores
|
||||
for j, s in zip(self.evaluation_stage, s_lst):
|
||||
if s is None:
|
||||
continue
|
||||
yield j[1]
|
||||
yield "学号"
|
||||
yield "姓名"
|
||||
for i in range(self.kpi_number):
|
||||
s_lst = self.achievement_level[class_index][i].scores
|
||||
for (index, j), s in zip(enumerate(self.evaluation_stage), s_lst):
|
||||
if s is None:
|
||||
continue
|
||||
match index:
|
||||
case 0:
|
||||
yield "\n".join([x for x in self.n_evaluation_methods[:3] if x is not None])
|
||||
case 1:
|
||||
yield "\n".join([x for x in self.n_evaluation_methods[3:5] if x is not None])
|
||||
case 2:
|
||||
if (len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] != "试卷" or
|
||||
len(self.n_evaluation_methods) == 5 and self.evaluation_stage[2][0] != "试卷"):
|
||||
yield self.evaluation_stage[2][0]
|
||||
else:
|
||||
# 中文数字到数字的映射
|
||||
chinese_num_map = {'一': 1, '二': 2, '三': 3,
|
||||
'四': 4, '五': 5, '六': 6,
|
||||
'七': 7, '八': 8, '九': 9}
|
||||
q_lst = self.question_data[f'目标{i + 1}']
|
||||
q_dict = {key: [item[1] for item in q_lst if item[0] == key] for key in
|
||||
set(key for key, _ in q_lst)}
|
||||
q_str_lst = [f"{k[0]}、{k[1:]}:{format_ranges(v)}" for k, v in q_dict.items()]
|
||||
q_str_lst.sort(key=lambda x: chinese_num_map[x[0]])
|
||||
q_str = "\n".join(q_str_lst)
|
||||
yield q_str
|
||||
case _:
|
||||
yield "如果你能看到本行文字,请联系开发者"
|
||||
|
||||
l_range, r_range = get_class_index_range(self.class_number, class_index)
|
||||
data = self.stu_kpi_achieve_list[l_range:r_range]
|
||||
# 要移除的索引位置
|
||||
remove_indices = [5, 9, 13, 17, 21]
|
||||
# 更新后的数据
|
||||
updated_data = [[item for j, item in enumerate(row) if j not in remove_indices] for row in data]
|
||||
for i in updated_data:
|
||||
for j in i:
|
||||
if j is None:
|
||||
continue
|
||||
yield j
|
||||
yield "平均得分"
|
||||
for i in range(self.kpi_number):
|
||||
avg_scores_lst = self.achievement_level[class_index][i].scores
|
||||
for j in avg_scores_lst:
|
||||
if j is None:
|
||||
continue
|
||||
yield j
|
||||
yield "得分率"
|
||||
for i in range(self.kpi_number):
|
||||
per_scores_lst = self.achievement_level[class_index][i].rates
|
||||
for j in per_scores_lst:
|
||||
if j is None:
|
||||
continue
|
||||
yield j
|
||||
yield "课程目标达成值"
|
||||
for i in range(self.kpi_number):
|
||||
yield f"课程目标{i + 1}达成值"
|
||||
rates_lst = []
|
||||
for x in range(self.kpi_number):
|
||||
rates_lst.append(self.achievement_level[class_index][x].rates)
|
||||
lst = [f"{r}×{p}" for (_, p), r in zip(self.evaluation_stage, rates_lst[i]) if r is not None]
|
||||
total = sum(p for (_, p), r in zip(self.evaluation_stage, rates_lst[i]) if r is not None)
|
||||
yield "({})/{}={}".format(
|
||||
"+".join(lst),
|
||||
total if total != 1 else 1,
|
||||
self.achievement_level[class_index][i].achievement)
|
||||
yield "考核材料清单"
|
||||
|
||||
y_str = (
|
||||
f"1.{self.evaluation_stage[0][0]}成绩:{'、'.join([x for x in self.n_evaluation_methods[:3] if x is not None])};"
|
||||
f"2.{self.evaluation_stage[1][0]}成绩:{'、'.join([x for x in self.n_evaluation_methods[3:5] if x is not None])};"
|
||||
)
|
||||
|
||||
# 7.7
|
||||
if len(self.n_evaluation_methods) == 6 and self.n_evaluation_methods[5] is not None:
|
||||
y_str += f"3.{self.evaluation_stage[2][0]}成绩:{self.n_evaluation_methods[5]}"
|
||||
else:
|
||||
y_str += f"3.{self.evaluation_stage[2][0] if self.evaluation_stage[2][0] != '试卷' else '期末'}成绩:{self.evaluation_stage[2][0]}"
|
||||
|
||||
yield y_str
|
||||
|
||||
if len(self.class_list) == 1:
|
||||
yield "课程目标达成值"
|
||||
for i in range(self.kpi_number):
|
||||
yield self.achievement_level[0][i].achievement
|
||||
|
||||
yield "课程目标达成结论"
|
||||
for i in range(self.kpi_number):
|
||||
if self.achievement_level[0][i].achievement >= 0.65:
|
||||
yield "R达成(≥0.65) £未达成(<0.65)"
|
||||
else:
|
||||
yield "£达成(≥0.65) R未达成(<0.65)"
|
||||
|
||||
yield "结果分析"
|
||||
|
||||
analysis_results = f"1.总体目标:本门课程有{self.kpi_number}个课程目标,"
|
||||
for index, p in enumerate(self.achievement_level[class_index]):
|
||||
analysis_results += f"课程目标{index + 1}达成值为{p.achievement},"
|
||||
if index + 1 == self.kpi_number:
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
for i in range(self.kpi_number):
|
||||
analysis_results += f"课程目标{i + 1}学生"
|
||||
if self.achievement_level[class_index][i].rates[0] is not None:
|
||||
analysis_results += \
|
||||
(f"在{self.evaluation_stage[0][0]}环节中达到"
|
||||
f"{get_rank(self.achievement_level[class_index][i].rates[0])}程度,")
|
||||
|
||||
if self.achievement_level[class_index][i].rates[1] is not None:
|
||||
analysis_results += \
|
||||
(f"在{self.evaluation_stage[1][0]}环节中"
|
||||
f"表现{get_rank(self.achievement_level[class_index][i].rates[1])},")
|
||||
|
||||
if self.achievement_level[class_index][i].rates[2] is not None:
|
||||
analysis_results += \
|
||||
(f"在{self.evaluation_stage[2][0] if self.evaluation_stage[2][0] != '试卷' else '考试'}"
|
||||
f"环节中为{get_rank(self.achievement_level[class_index][i].rates[2])}水平,")
|
||||
|
||||
analysis_results += f"总体目标{'达成' if self.achievement_level[class_index][i].achievement >= 0.65 else '未达成'};"
|
||||
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
c_lst = [x.achievement for x in self.achievement_level[class_index]]
|
||||
min_index = min(enumerate(c_lst), key=lambda x: x[1])[0]
|
||||
analysis_results += f"分析{self.kpi_number}个课程目标的达成值发现,课程目标{min_index + 1}的达成情况较差。"
|
||||
analysis_results += "\n2.个体差异:"
|
||||
for i in range(self.kpi_number):
|
||||
l, r = get_class_index_range(self.class_number, class_index)
|
||||
lst = min_score_people_name(self.stu_kpi_achieve_list, i, l, r)
|
||||
analysis_results += f"对于课程目标{i + 1},"
|
||||
if self.achievement_level[class_index][i].rates[0] is not None:
|
||||
analysis_results += \
|
||||
(f"{'、'.join(lst[0][:2])}{'等' if len(lst[0]) > 2 else ''}"
|
||||
f"在{self.evaluation_stage[0][0]}考核中能力较弱,")
|
||||
if self.achievement_level[class_index][i].rates[1] is not None:
|
||||
analysis_results += \
|
||||
(f"{'、'.join(lst[1][:2])}{'等' if len(lst[1]) > 2 else ''}"
|
||||
f"在{self.evaluation_stage[1][0]}考核中能力较弱,")
|
||||
if self.achievement_level[class_index][i].rates[2] is not None:
|
||||
analysis_results += \
|
||||
(f"{'、'.join(lst[2][:2])}{'等' if len(lst[2]) > 2 else ''}"
|
||||
f"在{self.evaluation_stage[2][0] if self.evaluation_stage[2][0] != '试卷' else '考试'}中能力较弱;")
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
yield analysis_results
|
||||
yield "改进措施"
|
||||
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||
"\n\n\n在这填入您的改进措施\n\n\n")
|
||||
for i in range(88888):
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
def get_word_template_part_2(self):
|
||||
# self.get_word_template_part0()
|
||||
yield "课程目标"
|
||||
for i in range(self.kpi_number):
|
||||
yield f"课程目标{i + 1} ({self.hml_list[i]})"
|
||||
yield ("总成绩=\n{}".
|
||||
format(
|
||||
"\n+".join([f"{n if n != '试卷' else '期末'}成绩×{int(p * 100)}%" for n, p in self.evaluation_stage if
|
||||
p is not None])))
|
||||
for i in self.course_objectives:
|
||||
yield i
|
||||
yield "支撑毕业要求指标点"
|
||||
for i in self.kpi_list:
|
||||
yield i
|
||||
yield "考核类型"
|
||||
for i in range(self.kpi_number):
|
||||
s_lst = self.achievement_level[0][i].scores
|
||||
for index, (j, s) in enumerate(zip(self.evaluation_stage, s_lst)):
|
||||
if s is None:
|
||||
continue
|
||||
if index == 2:
|
||||
yield f"期末考核\n({j[0]})"
|
||||
else:
|
||||
yield f"{j[0]}考核"
|
||||
|
||||
yield "目标分值(分)"
|
||||
for i in self.target_score:
|
||||
for j in i:
|
||||
if j is None:
|
||||
continue
|
||||
yield j
|
||||
yield "权重"
|
||||
for i in range(self.kpi_number):
|
||||
s_lst = self.achievement_level[0][i].scores
|
||||
for j, s in zip(self.evaluation_stage, s_lst):
|
||||
if s is None:
|
||||
continue
|
||||
yield j[1]
|
||||
min_rates = []
|
||||
for index, i in enumerate(self.class_list):
|
||||
yield i
|
||||
for a_index, j in enumerate(self.achievement_level[index]):
|
||||
if len(min_rates) - 1 < a_index:
|
||||
min_rates.append(j.achievement)
|
||||
else:
|
||||
min_rates[a_index] = min(min_rates[a_index], j.achievement)
|
||||
yield j.achievement
|
||||
yield "课程目标达成值\n(取各班级最小值)"
|
||||
for i in min_rates:
|
||||
yield i
|
||||
yield "课程目标达成结论"
|
||||
for i in min_rates:
|
||||
if i >= 0.65:
|
||||
yield "R达成(≥0.65) £未达成(<0.65)"
|
||||
else:
|
||||
yield "£达成(≥0.65) R未达成(<0.65)"
|
||||
yield "结果分析"
|
||||
analysis_results = f"1.总体情况:\n 本门课程有{self.kpi_number}个课程目标,"
|
||||
for i in range(self.kpi_number):
|
||||
analysis_results += f"课程目标{i + 1}达成值为{min_rates[i]},"
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
passed_c_lst = [f"课程目标{index + 1}" for index, x in enumerate(min_rates) if x >= 0.65]
|
||||
not_passed_c_lst = [f"课程目标{index + 1}" for index, x in enumerate(min_rates) if x < 0.65]
|
||||
if len(passed_c_lst) > 0:
|
||||
analysis_results += (f"{'、'.join(passed_c_lst)}达到0.65的达成标准,"
|
||||
f"表明学生在{'、'.join(passed_c_lst)}方面基本达到了本门课程提出的能力要求"
|
||||
f"{';' if len(not_passed_c_lst) > 0 else '。'}")
|
||||
if len(not_passed_c_lst) > 0:
|
||||
analysis_results += (f"{'、'.join(not_passed_c_lst)}未达到0.65的达成标准,"
|
||||
f"表明学生在{'、'.join(not_passed_c_lst)}方面未能达到课程提出的能力要求,"
|
||||
f"学生能力还有待进一步加强。")
|
||||
analysis_results += "\n2.班级差异:"
|
||||
avg_c_lst = [sum(i.achievement for i in x) / len(x) for x in self.achievement_level]
|
||||
top_index = max(enumerate(avg_c_lst), key=lambda x: x[1])[0]
|
||||
bottom_index = min(enumerate(avg_c_lst), key=lambda x: x[1])[0]
|
||||
analysis_results += (f"\n (1)班级对比:各班级之间比较而言,"
|
||||
f"{self.class_list[top_index]}班的课程目标达成值相对较高,"
|
||||
f"{self.class_list[bottom_index]}班的课程目标达成值较低。")
|
||||
analysis_results += f"\n (2)同班级不同课程目标比较:分析{self.kpi_number}个课程目标的达成值发现,"
|
||||
for i in range(len(self.class_list)):
|
||||
min_p_rate_index = min(enumerate(self.achievement_level[i]), key=lambda x: x[1].achievement)[0]
|
||||
o_c_str = [f'课程目标{x + 1}' for x in range(self.kpi_number) if x != min_p_rate_index]
|
||||
analysis_results += (f"{self.class_list[i]}班的课程目标{min_p_rate_index + 1}达成情况最低,"
|
||||
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement},"
|
||||
f"{'、'.join(o_c_str)}的达成情况较好;")
|
||||
analysis_results = analysis_results[:-1] + "。"
|
||||
analysis_results += "\n3.结果分析: \n在此填写您的结果分析\n\n"
|
||||
yield analysis_results
|
||||
yield "改进措施"
|
||||
yield "注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n\n\n\n"
|
||||
for i in range(88888):
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
@staticmethod
|
||||
def get_word_template_part_3():
|
||||
yield "课程目标达成情况合理性评价"
|
||||
yield "评价样本的合理性"
|
||||
yield "R全体样本 £抽样样本"
|
||||
yield "评价依据的合理性"
|
||||
yield "考核方法 R合适 £不合适 "
|
||||
yield "考核内容是否支撑课程目标 R是 £否"
|
||||
yield "评分标准 R明确 £不明确"
|
||||
yield "计算过程的合理性"
|
||||
yield "R合理正确 £合理但不够准确 £不合理"
|
||||
yield "问题分析的合理性"
|
||||
yield "R合理深入 £合理但不够深入 £不合理"
|
||||
yield "改进措施的合理性"
|
||||
yield "R合理可行 £合理但不太可行 £不合理"
|
||||
yield "合理性评价结果"
|
||||
yield "R合理 £基本合理 £不合理"
|
||||
yield "专业负责人/系主任(签字)"
|
||||
yield ("整改意见:\n"
|
||||
"\n\n\n\n\n"
|
||||
"\n\n\n"
|
||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
||||
yield "课程负责人(签字)"
|
||||
yield ("拟整改计划与措施:\n"
|
||||
"\n\n\n\n\n"
|
||||
"\n\n\n"
|
||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
||||
for i in range(88888):
|
||||
yield "如果您看到了本段文字,请联系开发者"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
excel_reader = ExcelReader('../files/21工管-房屋建筑学-点名册-1227.xlsm')
|
||||
excel_reader.parse_excel()
|
||||
s, e = get_class_index_range(excel_reader.class_number, 1)
|
||||
r = min_score_people_name(excel_reader.stu_kpi_achieve_list, 0, s, e)
|
||||
gen_picture(excel_reader.stu_kpi_achieve_list, 3, s, e)
|
||||
print()
|
||||
@@ -15,9 +15,9 @@ class DocPaper:
|
||||
|
||||
section = self._doc.sections[0]
|
||||
section.top_margin = Cm(2)
|
||||
section.bottom_margin = Cm(1)
|
||||
section.left_margin = Cm(1.4)
|
||||
section.right_margin = Cm(1.4)
|
||||
section.bottom_margin = Cm(2)
|
||||
section.left_margin = Cm(2)
|
||||
section.right_margin = Cm(2)
|
||||
|
||||
def add_paper(self, course: Course, student: Student):
|
||||
temp_table = self._template.tables[0]
|
||||
|
||||
13219
module/resources.py
Normal file
13219
module/resources.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from openpyxl.reader.excel import load_workbook
|
||||
|
||||
|
||||
@@ -31,6 +32,7 @@ class Question:
|
||||
if row[0] is None and row[1] is None:
|
||||
break
|
||||
questions.append(Question(*row))
|
||||
wb.close()
|
||||
return questions
|
||||
|
||||
@property
|
||||
@@ -64,6 +66,7 @@ class Student:
|
||||
students = []
|
||||
for row in ws.iter_rows(min_row=6, max_col=5, values_only=True):
|
||||
students.append(Student(*row))
|
||||
wb.close()
|
||||
return [x for x in students if x.valid]
|
||||
|
||||
@property
|
||||
@@ -115,6 +118,7 @@ class Course:
|
||||
wb = load_workbook(path, read_only=True)
|
||||
ws = wb.active
|
||||
name: str = ws['E3'].value
|
||||
wb.close()
|
||||
return Course(name[5:])
|
||||
|
||||
@property
|
||||
@@ -122,5 +126,15 @@ class Course:
|
||||
return self._name
|
||||
|
||||
|
||||
class Performance:
|
||||
def __init__(self, scores: Tuple[float, float, float], rates: Tuple[float, float, float], achievement: float):
|
||||
self.scores = scores # (平时平均分, 大作业平均分, 试卷平均分)
|
||||
self.rates = rates # (平时得分率, 大作业得分率, 试卷得分率)
|
||||
self.achievement = achievement # 达成度
|
||||
|
||||
def __str__(self):
|
||||
return f"Performance(scores={self.scores}, rates={self.rates}, achievement={self.achievement})"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
...
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from win32com import client
|
||||
|
||||
from module.achievement_doc import DocxWriter
|
||||
from module.achievement_excel import ExcelReader
|
||||
from module.doc import DocPaper
|
||||
from module.schema import Course, Student, Question
|
||||
from utils.function import resource_path
|
||||
@@ -11,6 +14,7 @@ from utils.function import resource_path
|
||||
class DTGWorker(QObject):
|
||||
progress = Signal(int)
|
||||
finished = Signal()
|
||||
error = Signal(str, str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -26,19 +30,63 @@ class DTGWorker(QObject):
|
||||
self.output_filename = output_filename
|
||||
|
||||
def run(self):
|
||||
course = Course.load_from_xls(self.input_filepath)
|
||||
students = Student.load_from_xls(self.input_filepath)
|
||||
questions = Question.load_from_xls(self.input_question_filepath)
|
||||
try:
|
||||
course = Course.load_from_xls(self.input_filepath)
|
||||
students = Student.load_from_xls(self.input_filepath)
|
||||
questions = Question.load_from_xls(self.input_question_filepath)
|
||||
|
||||
d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx"))
|
||||
for index, student in enumerate(students):
|
||||
if (p := int((index + 1) / len(students) * 100)) != 100:
|
||||
self.progress.emit(p)
|
||||
else:
|
||||
self.progress.emit(99)
|
||||
student.pick_question(questions)
|
||||
d.add_paper(course, student)
|
||||
d.save(self.output_filepath)
|
||||
self.progress.emit(100)
|
||||
os.startfile(Path(self.output_filepath) / f"{self.output_filename}.docx")
|
||||
self.finished.emit()
|
||||
d = DocPaper(self.output_filename, template_path=resource_path("template/template.docx"))
|
||||
for index, student in enumerate(students):
|
||||
if (p := int((index + 1) / len(students) * 100)) != 100:
|
||||
self.progress.emit(p)
|
||||
else:
|
||||
self.progress.emit(99)
|
||||
student.pick_question(questions)
|
||||
d.add_paper(course, student)
|
||||
d.save(self.output_filepath)
|
||||
self.progress.emit(100)
|
||||
|
||||
word_file = self.output_filepath + "/" + self.output_filename + ".docx"
|
||||
pdf_file = self.output_filepath + "/" + self.output_filename + ".pdf"
|
||||
|
||||
if os.path.exists(pdf_file):
|
||||
os.remove(pdf_file)
|
||||
|
||||
word = client.Dispatch("Word.Application")
|
||||
doc = word.Documents.Open(word_file)
|
||||
doc.SaveAs(pdf_file, 17)
|
||||
doc.Close()
|
||||
word.Quit()
|
||||
|
||||
os.remove(word_file)
|
||||
os.startfile(pdf_file)
|
||||
except Exception as _:
|
||||
error_msg = traceback.format_exc()
|
||||
self.error.emit("😢 不好出错了", error_msg)
|
||||
self.progress.emit(-1)
|
||||
finally:
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class ARGWorker(QObject):
|
||||
finished = Signal()
|
||||
error = Signal(str, str)
|
||||
|
||||
def __init__(self, input_filepath: str, output_filepath: str, output_filename: str, disable_cc: bool = False):
|
||||
super().__init__()
|
||||
self.input_filepath = input_filepath
|
||||
self.output_filepath = output_filepath
|
||||
self.output_filename = output_filename
|
||||
self.disable_compatibility_check = disable_cc
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
excel = ExcelReader(self.input_filepath, self.disable_compatibility_check)
|
||||
excel.run()
|
||||
|
||||
doc = DocxWriter(self.output_filepath, self.output_filename, excel)
|
||||
doc.write()
|
||||
except Exception as e:
|
||||
self.error.emit("😢 不好出错了", str(e))
|
||||
finally:
|
||||
self.finished.emit()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
openpyxl
|
||||
pyside6
|
||||
python-docx
|
||||
openpyxl~=3.1.5
|
||||
pyside6~=6.9.0
|
||||
python-docx~=1.1.2
|
||||
matplotlib~=3.10.3
|
||||
PySide6-Fluent-Widgets[full]
|
||||
packaging~=25.0
|
||||
6
resources.qrc
Normal file
6
resources.qrc
Normal file
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE RCC>
|
||||
<RCC version="1.0">
|
||||
<qresource>
|
||||
<file>./images/logo.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
Binary file not shown.
19
ui/main.py
19
ui/main.py
@@ -1,6 +1,8 @@
|
||||
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition
|
||||
from PySide6.QtGui import QIcon
|
||||
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox
|
||||
|
||||
from ui.pyui.about_ui import AboutWidget
|
||||
from ui.pyui.achievement_ui import AchievementWidget
|
||||
from ui.pyui.defense_ui import DefenseWidget
|
||||
|
||||
|
||||
@@ -8,16 +10,25 @@ class MainWindow(MSFluentWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.homeInterface = DefenseWidget('Defense Interface', self)
|
||||
self.achievementInterface = AchievementWidget('Achievement Interface', self)
|
||||
self.defenseInterface = DefenseWidget('Defense Interface', self)
|
||||
self.aboutInterface = AboutWidget('About Interface', self)
|
||||
|
||||
self.achievementInterface.error.connect(self.showError)
|
||||
self.defenseInterface.errorSignal.connect(self.showError)
|
||||
|
||||
self.initNavigation()
|
||||
self.initWindow()
|
||||
|
||||
def initNavigation(self):
|
||||
self.addSubInterface(self.homeInterface, FluentIcon.HOME, '首页')
|
||||
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
|
||||
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩')
|
||||
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
|
||||
|
||||
def initWindow(self):
|
||||
self.resize(900, 700)
|
||||
self.setWindowTitle('答辩题目生成器')
|
||||
self.setWindowTitle('建工工具箱')
|
||||
self.setWindowIcon(QIcon(':/images/logo.png'))
|
||||
|
||||
def showError(self, title: str, message: str):
|
||||
MessageBox(title, message, self).exec()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6.QtWidgets import QVBoxLayout
|
||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon, GroupHeaderCardWidget, PushButton
|
||||
from PySide6.QtGui import QDesktopServices, Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout
|
||||
from qfluentwidgets import PrimaryPushSettingCard, FluentIcon, GroupHeaderCardWidget, PushButton, ImageLabel, TitleLabel
|
||||
|
||||
from ui.components.widget import Widget
|
||||
|
||||
@@ -9,11 +9,19 @@ class AboutWidget(Widget):
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.logoImage = ImageLabel(':/images/logo.png')
|
||||
self.logoImage.scaledToHeight(100)
|
||||
self.appNameLabel = TitleLabel('建工工具箱🛠️')
|
||||
|
||||
self.hBox = QHBoxLayout()
|
||||
self.hBox.addWidget(self.logoImage, 0, Qt.AlignLeft)
|
||||
self.hBox.addWidget(self.appNameLabel, 1, Qt.AlignLeft)
|
||||
|
||||
self.version_card = PrimaryPushSettingCard(
|
||||
text="获取源码",
|
||||
icon=FluentIcon.INFO,
|
||||
title="关于",
|
||||
content="作者:许方杰。当前版本:1.0.0\n使用 GPLv3 开源协议,作者不对使用本软件造成的任何损失负责。"
|
||||
content="作者:许方杰。当前版本:1.0.0\n本软件使用 GPLv3 开源协议进行分发,作者不对使用本软件造成的任何损失负责。"
|
||||
)
|
||||
self.button_list = [
|
||||
PushButton("访问网站"),
|
||||
@@ -31,6 +39,7 @@ class AboutWidget(Widget):
|
||||
self.group_card.setTitle("第三方框架")
|
||||
self.vbox = QVBoxLayout(self)
|
||||
|
||||
self.vbox.addLayout(self.hBox)
|
||||
self.vbox.addWidget(self.version_card)
|
||||
self.vbox.addWidget(self.group_card)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
243
ui/pyui/achievement_ui.py
Normal file
243
ui/pyui/achievement_ui.py
Normal file
@@ -0,0 +1,243 @@
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Callable, Literal
|
||||
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout, QProgressBar
|
||||
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, InfoBarIcon, BodyLabel, \
|
||||
PrimaryPushButton, SwitchButton, MessageBox, InfoBar, InfoBarPosition, IndeterminateProgressBar
|
||||
|
||||
from module.worker import ARGWorker
|
||||
from ui.components.widget import Widget
|
||||
|
||||
|
||||
class InputSettingCard(GroupHeaderCardWidget):
|
||||
chooseSignal = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.chooseFileButton = PushButton("打开")
|
||||
|
||||
self.chooseFileButton.setFixedWidth(120)
|
||||
|
||||
self.inputGroup = self.addGroup(FluentIcon.DOCUMENT, "目标文件", "选择达成度计算表", self.chooseFileButton)
|
||||
|
||||
# ============================
|
||||
self.chooseFileButton.clicked.connect(self.choose_file)
|
||||
|
||||
def choose_file(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsm);")
|
||||
if file_path:
|
||||
self.inputGroup.setContent("已选择文件:" + file_path)
|
||||
self.chooseSignal.emit(file_path)
|
||||
|
||||
|
||||
class OutputSettingCard(GroupHeaderCardWidget):
|
||||
updateSignal = Signal(str, str)
|
||||
startSignal = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("输出选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.chooseExportDirectoryButton = PushButton("选择")
|
||||
self.exportFileNameLineEdit = LineEdit()
|
||||
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
|
||||
self.autoOpenSwitch = SwitchButton()
|
||||
self.disableCompatibilityCheckSwitch = SwitchButton()
|
||||
|
||||
self.autoOpenSwitch.setChecked(True)
|
||||
self.bottomLayout = QHBoxLayout()
|
||||
self.hintIcon = IconWidget(InfoBarIcon.INFORMATION)
|
||||
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
||||
self.startButton.setEnabled(False)
|
||||
|
||||
# 设置底部工具栏布局
|
||||
self.hintIcon.setFixedSize(16, 16)
|
||||
self.bottomLayout.setSpacing(10)
|
||||
self.bottomLayout.setContentsMargins(24, 15, 24, 20)
|
||||
self.bottomLayout.addWidget(self.hintIcon, 0, Qt.AlignLeft)
|
||||
self.bottomLayout.addWidget(self.hintLabel, 0, Qt.AlignLeft)
|
||||
self.bottomLayout.addStretch(1)
|
||||
self.bottomLayout.addWidget(self.startButton, 0, Qt.AlignRight)
|
||||
self.bottomLayout.setAlignment(Qt.AlignVCenter)
|
||||
|
||||
self.chooseExportDirectoryButton.setFixedWidth(120)
|
||||
self.exportFileNameLineEdit.setPlaceholderText("请输入文件名")
|
||||
self.startButton.setFixedWidth(120)
|
||||
self.exportFileNameLineEdit.setFixedWidth(360)
|
||||
self.exportFileNameLineEdit.setPlaceholderText("输入导出文件名,例如:21工程管理(1)达成度报告")
|
||||
|
||||
self.dirGroup = self.addGroup(FluentIcon.FOLDER, "导出目录", "选择导出文件的目录",
|
||||
self.chooseExportDirectoryButton)
|
||||
self.fnGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件名", "输入导出文件名", self.exportFileNameLineEdit)
|
||||
self.aoGroup = self.addGroup(FluentIcon.VIEW, "自动打开", "生成完成后自动打开文件", self.autoOpenSwitch)
|
||||
self.ccGroup = self.addGroup(FluentIcon.DEVELOPER_TOOLS, "关闭兼容性检查",
|
||||
"⚠ 注意:该功能为实验性内容,仅当你明确了解其影响并知道自己在做什么时,才建议关闭兼容性检查!",
|
||||
self.disableCompatibilityCheckSwitch)
|
||||
self.ccGroup.setSeparatorVisible(True)
|
||||
self.vBoxLayout.addLayout(self.bottomLayout)
|
||||
|
||||
# =============================
|
||||
self.chooseExportDirectoryButton.clicked.connect(self.choose_dir)
|
||||
self.startButton.clicked.connect(self.startSignal)
|
||||
|
||||
def choose_dir(self) -> None:
|
||||
dir_path = QFileDialog.getExistingDirectory(self, "选择文件夹", "")
|
||||
if dir_path:
|
||||
self.dirGroup.setContent(f"当前保存的文件目录:{dir_path}")
|
||||
self.updateSignal.emit('d', dir_path)
|
||||
|
||||
def update_path_and_filename_by_input_path(self, path: str):
|
||||
dir_path = path[:path.rfind("/")]
|
||||
self.dirGroup.setContent(f"已选择目录:{dir_path}")
|
||||
self.updateSignal.emit('d', dir_path)
|
||||
|
||||
file_name = path[path.rfind("/") + 1:path.rfind(".")] + "-达成度报告"
|
||||
self.exportFileNameLineEdit.setText(file_name)
|
||||
self.updateSignal.emit('f', file_name)
|
||||
|
||||
|
||||
class AchievementWidget(Widget):
|
||||
error = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
self.inputGroup = InputSettingCard(self)
|
||||
self.outputGroup = OutputSettingCard(self)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
|
||||
self.vbox.addWidget(self.inputGroup)
|
||||
self.vbox.addWidget(self.outputGroup)
|
||||
self.vbox.addStretch(1)
|
||||
|
||||
# =================================
|
||||
|
||||
self.infoBar = None
|
||||
|
||||
# =================================
|
||||
|
||||
self.thread = None
|
||||
self.worker = None
|
||||
|
||||
# ==================================
|
||||
|
||||
self.input_file_path = ""
|
||||
self.output_file_path = ""
|
||||
self.output_file_name = ""
|
||||
|
||||
# ==================================
|
||||
|
||||
self.inputGroup.chooseSignal.connect(self.input_signal_receive)
|
||||
self.outputGroup.updateSignal.connect(self.export_signal_receive)
|
||||
self.outputGroup.startSignal.connect(self.start_generate)
|
||||
|
||||
def enable_start_check(func: Callable):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
result = func(self, *args, **kwargs)
|
||||
|
||||
fields = [
|
||||
self.input_file_path,
|
||||
self.output_file_path,
|
||||
self.output_file_name,
|
||||
]
|
||||
if all(fields):
|
||||
self.outputGroup.startButton.setEnabled(True)
|
||||
else:
|
||||
self.outputGroup.startButton.setEnabled(False)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
@enable_start_check
|
||||
def set_value(
|
||||
self,
|
||||
key: Literal['input_file_path', 'output_file_path', 'output_file_name'],
|
||||
value: str
|
||||
) -> None:
|
||||
setattr(self, key, value)
|
||||
|
||||
def input_signal_receive(self, file_path: str) -> None:
|
||||
self.set_value('input_file_path', file_path)
|
||||
self.outputGroup.update_path_and_filename_by_input_path(file_path)
|
||||
|
||||
def export_signal_receive(self, s_type: Literal['d', 'f'], value: str) -> None:
|
||||
if s_type == 'd':
|
||||
self.set_value('output_file_path', value)
|
||||
elif s_type == 'f':
|
||||
self.set_value('output_file_name', value)
|
||||
|
||||
def start_generate(self):
|
||||
self.thread = QThread()
|
||||
self.worker = ARGWorker(
|
||||
self.input_file_path,
|
||||
self.output_file_path,
|
||||
self.output_file_name,
|
||||
disable_cc=self.outputGroup.disableCompatibilityCheckSwitch.isChecked()
|
||||
)
|
||||
self.worker.moveToThread(self.thread)
|
||||
|
||||
self.outputGroup.startButton.setEnabled(False)
|
||||
|
||||
# 线程启动与信号连接
|
||||
self.thread.started.connect(self.worker.run)
|
||||
|
||||
self.show_info_bar('')
|
||||
self.worker.error.connect(self.show_error)
|
||||
|
||||
self.worker.finished.connect(self.thread.quit)
|
||||
self.worker.finished.connect(self.worker.deleteLater)
|
||||
self.thread.finished.connect(self.thread.deleteLater)
|
||||
self.thread.finished.connect(self.clear_thread_worker_refs)
|
||||
self.thread.finished.connect(self.after_generate)
|
||||
|
||||
# 启动线程
|
||||
self.thread.start()
|
||||
|
||||
def clear_thread_worker_refs(self):
|
||||
self.thread = None
|
||||
self.worker = None
|
||||
|
||||
def after_generate(self):
|
||||
self.outputGroup.startButton.setEnabled(True)
|
||||
if self.outputGroup.autoOpenSwitch.isChecked():
|
||||
try:
|
||||
os.startfile(self.output_file_path + "/" + self.output_file_name + ".docx")
|
||||
except Exception as e:
|
||||
self.show_error("?? 不好出错了", str(e))
|
||||
if self.infoBar:
|
||||
self.infoBar.close()
|
||||
self.infoBar = InfoBar.success(
|
||||
title='成功!',
|
||||
content="正在打开文件" if self.outputGroup.autoOpenSwitch.isChecked() else "文件已保存",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=5000,
|
||||
parent=self
|
||||
)
|
||||
|
||||
def show_error(self, title: str, content: str):
|
||||
self.error.emit(title, content)
|
||||
|
||||
def show_info_bar(self, info: str):
|
||||
self.infoBar = InfoBar(
|
||||
icon=InfoBarIcon.INFORMATION,
|
||||
title='请稍后',
|
||||
content=info,
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=False,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
self.infoBar.addWidget(IndeterminateProgressBar(start=True))
|
||||
self.infoBar.show()
|
||||
@@ -4,7 +4,7 @@ from functools import wraps
|
||||
from PySide6.QtCore import Qt, Signal, QThread
|
||||
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog
|
||||
from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, InfoBarIcon, \
|
||||
BodyLabel, PrimaryPushButton, FluentIcon, LineEdit, InfoBar, InfoBarPosition, ProgressBar
|
||||
BodyLabel, PrimaryPushButton, FluentIcon, LineEdit, InfoBar, InfoBarPosition, ProgressBar, IndeterminateProgressBar
|
||||
|
||||
from module.worker import DTGWorker
|
||||
from ui.components.widget import Widget
|
||||
@@ -16,7 +16,7 @@ class InitSettingCard(GroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("初始化")
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.chooseStudentButton = PushButton("打开")
|
||||
@@ -53,7 +53,7 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("导出")
|
||||
self.setTitle("输入选项")
|
||||
self.setBorderRadius(8)
|
||||
|
||||
self.chooseExportDirectoryButton = PushButton("选择")
|
||||
@@ -100,14 +100,16 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
||||
|
||||
def update_export_setting_by_signal(self, path: str) -> None:
|
||||
f_dir = path[:path.rfind('/')]
|
||||
f_name = path[path.rfind('/') + 1:path.rfind('.')]
|
||||
f_name = path[path.rfind('/') + 1:path.rfind('.')] + '-答辩题目'
|
||||
self.dirGroup.setContent(f"当前保存的文件目录:{f_dir}")
|
||||
self.updateSignal.emit('d', f_dir)
|
||||
self.exportFileNameLineEdit.setText(f"{f_name}-答辩题目")
|
||||
self.exportFileNameLineEdit.setText(f_name)
|
||||
self.updateSignal.emit('n', f_name)
|
||||
|
||||
|
||||
class DefenseWidget(Widget):
|
||||
errorSignal = Signal(str, str)
|
||||
|
||||
def __init__(self, key: str, parent=None):
|
||||
super().__init__(key, parent)
|
||||
|
||||
@@ -125,6 +127,8 @@ class DefenseWidget(Widget):
|
||||
self.thread = None
|
||||
self.worker = None
|
||||
|
||||
self.successFlag = True
|
||||
|
||||
# ===================================
|
||||
|
||||
self.input_student_filepath = None
|
||||
@@ -157,17 +161,21 @@ class DefenseWidget(Widget):
|
||||
self.pb.setValue(value)
|
||||
if value == 100:
|
||||
self.infoBar.close()
|
||||
self.infoBar = InfoBar.success(
|
||||
title='成功!',
|
||||
content="正在打开文件...",
|
||||
self.infoBar = InfoBar(
|
||||
icon=InfoBarIcon.INFORMATION,
|
||||
title='正在转换文件',
|
||||
content="",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
isClosable=False,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=5000,
|
||||
duration=-1,
|
||||
parent=self
|
||||
)
|
||||
self.infoBar.addWidget(IndeterminateProgressBar(start=True))
|
||||
self.infoBar.show()
|
||||
self.pb = None
|
||||
self.exportCard.startButton.setEnabled(True)
|
||||
elif value == -1:
|
||||
self.successFlag = False
|
||||
|
||||
def enable_start_check(func: Callable):
|
||||
@wraps(func)
|
||||
@@ -198,7 +206,6 @@ class DefenseWidget(Widget):
|
||||
self.output_filepath = value
|
||||
elif key == "output_filename":
|
||||
self.output_filename = value
|
||||
setattr(self, key, value)
|
||||
|
||||
def input_signal_receive(self, s_type: Literal['s', 'q'], value: str):
|
||||
if s_type == "s":
|
||||
@@ -224,16 +231,19 @@ class DefenseWidget(Widget):
|
||||
self.worker.moveToThread(self.thread)
|
||||
|
||||
self.show_info_bar()
|
||||
self.successFlag = True
|
||||
self.exportCard.startButton.setEnabled(False)
|
||||
|
||||
# 线程启动与信号连接
|
||||
self.thread.started.connect(self.worker.run)
|
||||
self.worker.progress.connect(self.set_pb_value)
|
||||
self.worker.error.connect(self.show_error)
|
||||
|
||||
self.worker.finished.connect(self.thread.quit)
|
||||
self.worker.finished.connect(self.worker.deleteLater)
|
||||
self.thread.finished.connect(self.thread.deleteLater)
|
||||
self.thread.finished.connect(self.clear_thread_worker_refs)
|
||||
self.thread.finished.connect(self.after_generate)
|
||||
|
||||
# 启动线程
|
||||
self.thread.start()
|
||||
@@ -241,3 +251,30 @@ class DefenseWidget(Widget):
|
||||
def clear_thread_worker_refs(self):
|
||||
self.thread = None
|
||||
self.worker = None
|
||||
|
||||
def after_generate(self):
|
||||
self.exportCard.startButton.setEnabled(True)
|
||||
self.infoBar.close()
|
||||
if self.successFlag:
|
||||
self.infoBar = InfoBar.success(
|
||||
title='成功!',
|
||||
content="正在打开文件...",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=5000,
|
||||
parent=self
|
||||
)
|
||||
else:
|
||||
self.infoBar = InfoBar.error(
|
||||
title='失败!',
|
||||
content="",
|
||||
orient=Qt.Horizontal,
|
||||
isClosable=True,
|
||||
position=InfoBarPosition.BOTTOM,
|
||||
duration=5000,
|
||||
parent=self
|
||||
)
|
||||
|
||||
def show_error(self, title: str, content: str):
|
||||
self.errorSignal.emit(title, content)
|
||||
|
||||
@@ -1,5 +1,185 @@
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import matplotlib
|
||||
from matplotlib import pyplot as plt
|
||||
import re
|
||||
|
||||
|
||||
def format_ranges(nums):
|
||||
"""格式化区间,支持特殊标记"""
|
||||
if not nums:
|
||||
return ""
|
||||
|
||||
def parse_value(val):
|
||||
"""解析值,返回主数值和次要标记"""
|
||||
if isinstance(val, int):
|
||||
return val, ""
|
||||
match = re.match(r"(\d+)[((](.*?)[))]", str(val))
|
||||
if match:
|
||||
return int(match.group(1)), match.group(2)
|
||||
return int(val), ""
|
||||
|
||||
# 排序规则:主数值优先,次要标记其次
|
||||
sorted_nums = sorted(nums, key=lambda x: parse_value(x))
|
||||
|
||||
result = []
|
||||
start = sorted_nums[0]
|
||||
end = sorted_nums[0]
|
||||
|
||||
def is_continuous(val1, val2):
|
||||
"""判断两个值是否连续"""
|
||||
main1, sub1 = parse_value(val1)
|
||||
main2, sub2 = parse_value(val2)
|
||||
# 主数值连续,且次要标记为空(特殊标记视为不连续)
|
||||
return main2 == main1 + 1 and not sub2
|
||||
|
||||
for i in range(1, len(sorted_nums)):
|
||||
if is_continuous(end, sorted_nums[i]):
|
||||
end = sorted_nums[i]
|
||||
else:
|
||||
# 处理一个区间
|
||||
if parse_value(start)[0] == parse_value(end)[0]: # 单值或特殊标记
|
||||
result.append(str(start))
|
||||
elif parse_value(end)[0] - parse_value(start)[0] >= 2: # 连续区间
|
||||
result.append(f"{parse_value(start)[0]}~{parse_value(end)[0]}")
|
||||
else: # 非连续的单值
|
||||
result.extend(map(str, [start, end]))
|
||||
start = end = sorted_nums[i]
|
||||
|
||||
# 添加最后一段区间
|
||||
if parse_value(start)[0] == parse_value(end)[0]: # 单值或特殊标记
|
||||
result.append(str(start))
|
||||
elif parse_value(end)[0] - parse_value(start)[0] >= 2: # 连续区间
|
||||
result.append(f"{parse_value(start)[0]}~{parse_value(end)[0]}")
|
||||
else: # 非连续的单值
|
||||
result.extend(map(str, [start, end]))
|
||||
|
||||
return "、".join(result)
|
||||
|
||||
|
||||
def support_version_str(lst: list[str]) -> str:
|
||||
"""支持的版本字符串"""
|
||||
if len(lst) == 1:
|
||||
return lst[0]
|
||||
elif len(lst) == 2:
|
||||
return f"{lst[0]}、{lst[1]}"
|
||||
else:
|
||||
return f"{lst[0]}~{lst[-1]}"
|
||||
|
||||
|
||||
def get_rank(score: int | float) -> str:
|
||||
"""根据分数获取等级"""
|
||||
if score >= 0.9:
|
||||
return "优秀"
|
||||
elif score >= 0.8:
|
||||
return "良好"
|
||||
elif score >= 0.7:
|
||||
return "中等"
|
||||
elif score >= 0.6:
|
||||
return "及格"
|
||||
else:
|
||||
return "不及格"
|
||||
|
||||
|
||||
def min_score_people_name(data: list, kpi_number: int, start_index: int, end_index: int) -> list[tuple[str]]:
|
||||
"""获取最低分人名"""
|
||||
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13
|
||||
# 20060510XX 覃XX 40.5 45 49 0.738 16 17 0 0.330 24.5 15 14 0.695
|
||||
remove_indices = [5, 9, 13, 17, 21]
|
||||
# 更新后的数据
|
||||
# 0 1 2 3 4 5 6 7 8 9 10
|
||||
# 20060510XX 覃XX 40.5 45 49 16 17 0 24.5 15 14
|
||||
index = 2 + kpi_number * 3
|
||||
updated_data = [[item for j, item in enumerate(row) if j not in remove_indices] for row in data]
|
||||
result = [(), (), ()]
|
||||
min_scores = [min(updated_data[start_index:end_index],
|
||||
key=lambda x: x[index + i] if x[index + i] is not None else 0)[index + i] for i in range(3)]
|
||||
for row in updated_data[start_index:end_index]:
|
||||
for i in range(3):
|
||||
if row[index + i] == min_scores[i]:
|
||||
result[i] += (row[1],)
|
||||
return result
|
||||
|
||||
|
||||
def get_class_index_range(lst: list[int], num: int):
|
||||
"""获取班级的索引范围"""
|
||||
if num < 0 or num > len(lst):
|
||||
raise ValueError("OutOfRange")
|
||||
# 计算前num-1项和加一
|
||||
first_value = sum(lst[:num])
|
||||
# 计算前num项和加一
|
||||
second_value = sum(lst[:num + 1]) if num < len(lst) else "OutOfRange"
|
||||
return (first_value, second_value)
|
||||
|
||||
|
||||
def check_version(version, compatible_versions):
|
||||
# 将版本字符串转换为数字列表以便比较
|
||||
version_nums = [int(v) for v in version.split('.')]
|
||||
min_version_nums = [int(v) for v in compatible_versions[0].split('.')]
|
||||
max_version_nums = [int(v) for v in compatible_versions[-1].split('.')]
|
||||
|
||||
# 比较版本
|
||||
if version_nums < min_version_nums:
|
||||
return False, "不支持低于{}的版本,如需尝试生成请勾选‘关闭兼容性检查’".format(compatible_versions[0])
|
||||
elif version_nums > max_version_nums:
|
||||
return True, "版本过高可能会导致兼容性问题"
|
||||
else:
|
||||
return True, ""
|
||||
|
||||
|
||||
def gen_picture(data: list[list[str]], kpi_number: int, start_index: int, end_index: int):
|
||||
# 设置全局字体
|
||||
matplotlib.rcParams['font.family'] = ['Times New Roman', 'KaiTi']
|
||||
matplotlib.rcParams['font.size'] = 10
|
||||
|
||||
# 将厘米转换为英寸
|
||||
width_in_inches = 8 * 0.393701
|
||||
height_in_inches = 6 * 0.393701
|
||||
|
||||
# 创建一个图形和三个子图
|
||||
fig, axs = plt.subplots(1, kpi_number, figsize=(width_in_inches * kpi_number, height_in_inches))
|
||||
|
||||
# 遍历每个指定的索引来生成散点图
|
||||
for ax, index in zip(axs, range(kpi_number)):
|
||||
# 处理数据
|
||||
data_indices = [5, 9, 13, 17, 21]
|
||||
update_data = [[float(item) for j, item in enumerate(row) if j in data_indices] for row in data]
|
||||
x = [i for i in range(end_index - start_index)]
|
||||
y = [row[index] for row in update_data[start_index:end_index]]
|
||||
|
||||
# 绘制散点图
|
||||
ax.scatter(x, y, label=f'目标{index + 1}达成值', s=10)
|
||||
# 添加期望值直线
|
||||
ax.axhline(y=0.65, color='r', linestyle='-', label='期望值')
|
||||
# 设置轴标签
|
||||
ax.set_xlabel('学生学号')
|
||||
ax.set_ylabel('达成值')
|
||||
# 设置y轴范围和刻度
|
||||
ax.set_ylim(0, 1)
|
||||
ax.set_yticks([i * 0.2 for i in range(6)])
|
||||
# 设置图例
|
||||
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.35), ncol=2, frameon=False)
|
||||
# 设置坐标原点为(0, 0)
|
||||
ax.set_xlim(x.index(x[0]), None)
|
||||
|
||||
# 调整子图间距
|
||||
plt.tight_layout()
|
||||
# 创建一个内存中的文件对象
|
||||
buf = io.BytesIO()
|
||||
# 保存图形
|
||||
plt.savefig(buf, format='png', dpi=300, bbox_inches='tight')
|
||||
# 将文件对象转换为字节对象
|
||||
res = buf.getvalue()
|
||||
buf.close()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
nums = [1, 2, 3, '4(1)', '4(2)']
|
||||
formatted_ranges = format_ranges(nums)
|
||||
print(formatted_ranges)
|
||||
|
||||
|
||||
def resource_path(relative_path: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user