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 Exception(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: if self.n_evaluation_methods[5] == "试卷": yield "期末考核\n(试卷)" else: yield "期末考核" elif j[0] == "试卷": yield "期末考核\n(试卷)" else: yield "期末考核" 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] != "试卷": yield self.n_evaluation_methods[5] elif 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()