Compare commits
17 Commits
eb9b8f5464
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 96350bb8e2 | |||
| 28e35ea429 | |||
| a708bbfa72 | |||
| db53baba23 | |||
| d95bdef3f5 | |||
| 1a00811cfc | |||
| 836ccbe4aa | |||
| a2f453a638 | |||
| 7f23d64eb2 | |||
| 475d23f49e | |||
| 85697f35c6 | |||
| de0655adbc | |||
| 8f8e1ed1aa | |||
| 39fcaf35ca | |||
| da723409ca | |||
| 438cb8a1d9 | |||
| 5749dbbcee |
21
build.bat
21
build.bat
@@ -1,37 +1,36 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal
|
||||||
chcp 65001
|
|
||||||
|
|
||||||
echo === 尝试激活虚拟环境 ===
|
echo === Activating virtual environment ===
|
||||||
if exist ".venv\Scripts\activate.bat" (
|
if exist ".venv\Scripts\activate.bat" (
|
||||||
call .venv\Scripts\activate.bat
|
call .venv\Scripts\activate.bat
|
||||||
) else (
|
) else (
|
||||||
echo 未发现虚拟环境,尝试创建中...
|
echo Virtual environment not found. Creating...
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [错误] 创建虚拟环境失败,请确认是否已安装 Python。
|
echo [ERROR] Failed to create virtual environment. Please ensure Python is installed.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
echo 虚拟环境创建成功,开始激活...
|
echo Virtual environment created. Activating...
|
||||||
call .venv\Scripts\activate.bat
|
call .venv\Scripts\activate.bat
|
||||||
)
|
)
|
||||||
|
|
||||||
echo === 安装依赖项 ===
|
echo === Installing dependencies ===
|
||||||
pip install -r requirements.txt
|
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple -r requirements.txt
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [错误] pip 安装依赖失败!
|
echo [ERROR] pip failed to install dependencies.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo === 使用 pyinstaller 构建 ===
|
echo === Building with pyinstaller ===
|
||||||
python .\utils\hook.py
|
python .\utils\hook.py
|
||||||
pyinstaller .\main.spec
|
pyinstaller .\main.spec
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [错误] 构建失败!
|
echo [ERROR] Build failed.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo === 构建完成 ===
|
echo === Build completed ===
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||||
# #
|
# #
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -155,10 +155,15 @@ class DocxWriter:
|
|||||||
for performance in self.excel_reader.achievement_level[r_index]:
|
for performance in self.excel_reader.achievement_level[r_index]:
|
||||||
non_none_count = 3 - performance.scores.count(None)
|
non_none_count = 3 - performance.scores.count(None)
|
||||||
if non_none_count > 1:
|
if non_none_count > 1:
|
||||||
cell_start = table.cell(row, col_span)
|
try:
|
||||||
cell_end = table.cell(row, col_span + non_none_count - 1)
|
cell_start = table.cell(row, col_span)
|
||||||
cell_start.merge(cell_end)
|
cell_end = table.cell(row, col_span + non_none_count - 1)
|
||||||
col_span += non_none_count
|
cell_start.merge(cell_end)
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
# self.signal(f"单元格合并失败:({row}, {col_span}),需要自行检查表格准确性",
|
||||||
|
# LOGLEVEL.WARNING)
|
||||||
|
col_span += non_none_count
|
||||||
|
|
||||||
start = rows - X + 3 + self.excel_reader.kpi_number
|
start = rows - X + 3 + self.excel_reader.kpi_number
|
||||||
if len(self.excel_reader.class_list) == 1:
|
if len(self.excel_reader.class_list) == 1:
|
||||||
@@ -239,8 +244,8 @@ class DocxWriter:
|
|||||||
f". 课程目标达成情况的合理性评价")
|
f". 课程目标达成情况的合理性评价")
|
||||||
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
|
self.set_run_font(run, 14, 'Times New Roman', '黑体', True)
|
||||||
|
|
||||||
rows = 9
|
rows = 11
|
||||||
cols = 4
|
cols = 6
|
||||||
table = doc.add_table(rows=rows, cols=cols)
|
table = doc.add_table(rows=rows, cols=cols)
|
||||||
# 设置外侧框线粗1.5磅,内侧框线粗0.5磅
|
# 设置外侧框线粗1.5磅,内侧框线粗0.5磅
|
||||||
self.set_table_borders(table)
|
self.set_table_borders(table)
|
||||||
@@ -250,13 +255,25 @@ class DocxWriter:
|
|||||||
cell_end = table.cell(0, cols - 1)
|
cell_end = table.cell(0, cols - 1)
|
||||||
cell_start.merge(cell_end)
|
cell_start.merge(cell_end)
|
||||||
# 合并第二行至最后
|
# 合并第二行至最后
|
||||||
for i in range(1, 9):
|
for i in range(1, rows):
|
||||||
if i == 2:
|
match i:
|
||||||
continue
|
case 2:
|
||||||
cell_start = table.cell(i, 1)
|
table.cell(i, 2).merge(table.cell(i, 3))
|
||||||
cell_end = table.cell(i, cols - 1)
|
table.cell(i, 4).merge(table.cell(i, 5))
|
||||||
cell_start.merge(cell_end)
|
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.put_data_to_table(table, self.excel_reader.get_word_template_part_3)
|
||||||
|
|
||||||
# 应用样式
|
# 应用样式
|
||||||
@@ -276,9 +293,37 @@ class DocxWriter:
|
|||||||
|
|
||||||
for t_index, table in enumerate(doc.tables):
|
for t_index, table in enumerate(doc.tables):
|
||||||
self.set_table_borders(table)
|
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):
|
for r_index, row in enumerate(table.rows):
|
||||||
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
|
row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
|
||||||
row.height = Cm(0.7)
|
# 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):
|
for c_index, cell in enumerate(row.cells):
|
||||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
|
||||||
self.set_cell_margins(cell, start=57, end=57)
|
self.set_cell_margins(cell, start=57, end=57)
|
||||||
@@ -336,6 +381,8 @@ class DocxWriter:
|
|||||||
(6, 1),
|
(6, 1),
|
||||||
(7, 1),
|
(7, 1),
|
||||||
(8, 1),
|
(8, 1),
|
||||||
|
(9, 1),
|
||||||
|
(10, 1),
|
||||||
]
|
]
|
||||||
if r_index == 0:
|
if r_index == 0:
|
||||||
for run in paragraph.runs:
|
for run in paragraph.runs:
|
||||||
@@ -379,6 +426,7 @@ class DocxWriter:
|
|||||||
"""
|
"""
|
||||||
设置单元格边框
|
设置单元格边框
|
||||||
kwargs: top, bottom, left, right, inside_h, inside_v
|
kwargs: top, bottom, left, right, inside_h, inside_v
|
||||||
|
值为0时移除边框,值大于0时设置边框粗细
|
||||||
"""
|
"""
|
||||||
tc = cell._tc
|
tc = cell._tc
|
||||||
tcPr = tc.get_or_add_tcPr()
|
tcPr = tc.get_or_add_tcPr()
|
||||||
@@ -391,10 +439,14 @@ class DocxWriter:
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
tag = 'w:{}'.format(key)
|
tag = 'w:{}'.format(key)
|
||||||
border = OxmlElement(tag)
|
border = OxmlElement(tag)
|
||||||
border.set(qn('w:val'), 'single')
|
if value == 0:
|
||||||
border.set(qn('w:sz'), str(int(value * 8)))
|
# 移除边框
|
||||||
border.set(qn('w:space'), '0')
|
border.set(qn('w:val'), 'nil')
|
||||||
border.set(qn('w:color'), 'auto')
|
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)
|
tcBorders.append(border)
|
||||||
|
|
||||||
# 将边框添加到单元格属性中
|
# 将边框添加到单元格属性中
|
||||||
@@ -490,6 +542,15 @@ class DocxWriter:
|
|||||||
run = paragraph.add_run()
|
run = paragraph.add_run()
|
||||||
run.add_picture(image_stream, width=width, height=height)
|
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):
|
def is_chinese(self, char):
|
||||||
"""判断字符是否为中文"""
|
"""判断字符是否为中文"""
|
||||||
if '\u4e00' <= char <= '\u9fff':
|
if '\u4e00' <= char <= '\u9fff':
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||||
# #
|
# #
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import traceback
|
import traceback
|
||||||
|
import io
|
||||||
from typing import Optional, Callable
|
from typing import Optional, Callable
|
||||||
|
|
||||||
import openpyxl
|
import openpyxl
|
||||||
@@ -22,6 +23,7 @@ from openpyxl.utils import get_column_letter, column_index_from_string
|
|||||||
from openpyxl.workbook.workbook import Workbook
|
from openpyxl.workbook.workbook import Workbook
|
||||||
from openpyxl.worksheet.worksheet import Worksheet
|
from openpyxl.worksheet.worksheet import Worksheet
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from module import LOGLEVEL, COMPATIBLE_VERSION
|
from module import LOGLEVEL, COMPATIBLE_VERSION
|
||||||
from module.schema import Performance
|
from module.schema import Performance
|
||||||
@@ -48,6 +50,35 @@ class ExcelReader:
|
|||||||
question_data: dict[str, list[tuple[str, int]]]
|
question_data: dict[str, list[tuple[str, int]]]
|
||||||
ignore_version_check: bool
|
ignore_version_check: bool
|
||||||
pic_list: list
|
pic_list: list
|
||||||
|
suggestion_template_list: list[Optional[str]]
|
||||||
|
major_director_signature_image: Optional[Image.Image]
|
||||||
|
course_leader_signature_image: Optional[Image.Image]
|
||||||
|
|
||||||
|
class _SheetImageLoader:
|
||||||
|
"""Lightweight image loader scoped for ExcelReader use."""
|
||||||
|
|
||||||
|
def __init__(self, sheet: Worksheet):
|
||||||
|
self._images: dict[str, Callable[[], bytes]] = {}
|
||||||
|
for image in getattr(sheet, "_images", []):
|
||||||
|
row = image.anchor._from.row + 1
|
||||||
|
col = get_column_letter(image.anchor._from.col + 1)
|
||||||
|
self._images[f"{col}{row}"] = image._data
|
||||||
|
|
||||||
|
def image_in(self, cell: str) -> bool:
|
||||||
|
return cell in self._images
|
||||||
|
|
||||||
|
def get(self, cell: str) -> Image.Image:
|
||||||
|
if cell not in self._images:
|
||||||
|
raise ValueError(f"Cell {cell} doesn't contain an image")
|
||||||
|
image = io.BytesIO(self._images[cell]())
|
||||||
|
return Image.open(image)
|
||||||
|
|
||||||
|
class ValidError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
def __init__(self, file_path: str, version_check: bool = False,
|
def __init__(self, file_path: str, version_check: bool = False,
|
||||||
signal: Callable[[str, str], None] = lambda x, y: print(x)):
|
signal: Callable[[str, str], None] = lambda x, y: print(x)):
|
||||||
@@ -73,6 +104,9 @@ class ExcelReader:
|
|||||||
self.ignore_version_check = version_check
|
self.ignore_version_check = version_check
|
||||||
self.pic_list = []
|
self.pic_list = []
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
|
self.suggestion_template_list = []
|
||||||
|
self.major_director_signature_image = None
|
||||||
|
self.course_leader_signature_image = None
|
||||||
|
|
||||||
def parse_excel(self):
|
def parse_excel(self):
|
||||||
try:
|
try:
|
||||||
@@ -80,6 +114,7 @@ class ExcelReader:
|
|||||||
sheet: Worksheet = wb['初始录入']
|
sheet: Worksheet = wb['初始录入']
|
||||||
# 读取版本号
|
# 读取版本号
|
||||||
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
|
e_version = sheet['V4'].value if sheet['V4'].value is not None else sheet['U4'].value
|
||||||
|
e_version = sheet['H1'].value if e_version is None else e_version
|
||||||
if e_version is None:
|
if e_version is None:
|
||||||
e_version = "0"
|
e_version = "0"
|
||||||
status, _ = check_version(e_version, COMPATIBLE_VERSION)
|
status, _ = check_version(e_version, COMPATIBLE_VERSION)
|
||||||
@@ -101,6 +136,13 @@ class ExcelReader:
|
|||||||
# 读取课程负责人
|
# 读取课程负责人
|
||||||
self.course_lead_teacher_name = sheet["D8"].value
|
self.course_lead_teacher_name = sheet["D8"].value
|
||||||
|
|
||||||
|
need_signature_images = CUR_VERSION >= version.parse("9.4") and sheet["H10"].value == "是"
|
||||||
|
if need_signature_images:
|
||||||
|
self._load_signature_images()
|
||||||
|
else:
|
||||||
|
self.major_director_signature_image = None
|
||||||
|
self.course_leader_signature_image = None
|
||||||
|
|
||||||
# 读取班级和人数
|
# 读取班级和人数
|
||||||
max_class_size = 4
|
max_class_size = 4
|
||||||
match CUR_VERSION:
|
match CUR_VERSION:
|
||||||
@@ -225,9 +267,26 @@ class ExcelReader:
|
|||||||
else:
|
else:
|
||||||
self.question_data[key] = [values]
|
self.question_data[key] = [values]
|
||||||
|
|
||||||
self.validate_data()
|
# 读取建议模板
|
||||||
|
if CUR_VERSION >= version.parse("9.0"):
|
||||||
|
sheet = wb['初始录入']
|
||||||
|
|
||||||
|
for i in range(29, 34):
|
||||||
|
self.suggestion_template_list.append(sheet[f'I{i}'].value)
|
||||||
|
|
||||||
|
if len(self.suggestion_template_list) != 5:
|
||||||
|
for i in range(len(self.suggestion_template_list), 5):
|
||||||
|
self.suggestion_template_list.append(None)
|
||||||
|
|
||||||
|
if vd_lst := self.validate_data():
|
||||||
|
raise self.ValidError("\n\n".join(vd_lst))
|
||||||
|
|
||||||
self.gen_picture()
|
self.gen_picture()
|
||||||
|
|
||||||
|
except self.ValidError as ve:
|
||||||
|
raise Exception(f"""
|
||||||
|
数据验证失败:\n\n{str(ve)}
|
||||||
|
""")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = traceback.format_exc()
|
error_message = traceback.format_exc()
|
||||||
raise Exception(f"""
|
raise Exception(f"""
|
||||||
@@ -238,13 +297,40 @@ class ExcelReader:
|
|||||||
def set_file_path(self, file_path: str):
|
def set_file_path(self, file_path: str):
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
|
|
||||||
def validate_data(self):
|
def validate_data(self) -> list[str]:
|
||||||
|
lst: list[str] = []
|
||||||
self.signal("正在验证数据", LOGLEVEL.INFO)
|
self.signal("正在验证数据", LOGLEVEL.INFO)
|
||||||
return 0
|
if len(self.kpi_list) != self.kpi_number:
|
||||||
|
self.signal("\"课程目标\"或\"目标支撑的毕业要求指标点\"数量与期望目标数量不符", LOGLEVEL.ERROR)
|
||||||
|
lst.append(
|
||||||
|
f"\"课程目标\"或\"目标支撑的毕业要求指标点\"数量与期望目标数量不符,请检查Excel表格中的\"课程目标\"和\"目标支撑的毕业要求指标点\"列是否填写完整。"
|
||||||
|
f"期望得到 {self.kpi_number} 个,实际检测到 {len(self.kpi_list)} 个。"
|
||||||
|
f"(如想暂时不填,请在Excel表格对应的位置添加一个空格)"
|
||||||
|
)
|
||||||
|
|
||||||
|
return lst
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.parse_excel()
|
self.parse_excel()
|
||||||
|
|
||||||
|
def _load_signature_images(self):
|
||||||
|
signature_cells = {
|
||||||
|
"major_director_signature_image": ("I34", "K34"),
|
||||||
|
"course_leader_signature_image": ("I35", "K35"),
|
||||||
|
}
|
||||||
|
wb_with_images: Workbook = openpyxl.load_workbook(self.file_path, data_only=True)
|
||||||
|
try:
|
||||||
|
sheet_with_images: Worksheet = wb_with_images["初始录入"]
|
||||||
|
loader = self._SheetImageLoader(sheet_with_images)
|
||||||
|
|
||||||
|
for attr, (check_cell, image_cell) in signature_cells.items():
|
||||||
|
if sheet_with_images[check_cell].value and loader.image_in(image_cell):
|
||||||
|
setattr(self, attr, loader.get(image_cell))
|
||||||
|
else:
|
||||||
|
setattr(self, attr, None)
|
||||||
|
finally:
|
||||||
|
wb_with_images.close()
|
||||||
|
|
||||||
def clear_all_data(self):
|
def clear_all_data(self):
|
||||||
self.kpi_list = []
|
self.kpi_list = []
|
||||||
self.kpi_number = 0
|
self.kpi_number = 0
|
||||||
@@ -264,6 +350,8 @@ class ExcelReader:
|
|||||||
self.hml_list = []
|
self.hml_list = []
|
||||||
self.question_data = {}
|
self.question_data = {}
|
||||||
self.pic_list = []
|
self.pic_list = []
|
||||||
|
self.major_director_signature_image = None
|
||||||
|
self.course_leader_signature_image = None
|
||||||
|
|
||||||
def set_version_check(self, version_check: bool):
|
def set_version_check(self, version_check: bool):
|
||||||
self.ignore_version_check = version_check
|
self.ignore_version_check = version_check
|
||||||
@@ -471,8 +559,8 @@ class ExcelReader:
|
|||||||
yield analysis_results
|
yield analysis_results
|
||||||
yield "改进措施"
|
yield "改进措施"
|
||||||
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||||
"\n\n\n在这填入您的改进措施\n\n\n")
|
f"{self.suggestion_template_list[0] if self.suggestion_template_list[0] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
|
||||||
for i in range(88888):
|
while True:
|
||||||
yield "如果您看到了本段文字,请联系开发者"
|
yield "如果您看到了本段文字,请联系开发者"
|
||||||
|
|
||||||
def get_word_template_part_2(self):
|
def get_word_template_part_2(self):
|
||||||
@@ -561,20 +649,21 @@ class ExcelReader:
|
|||||||
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement},"
|
f"达成值为{self.achievement_level[i][min_p_rate_index].achievement},"
|
||||||
f"{'、'.join(o_c_str)}的达成情况较好;")
|
f"{'、'.join(o_c_str)}的达成情况较好;")
|
||||||
analysis_results = analysis_results[:-1] + "。"
|
analysis_results = analysis_results[:-1] + "。"
|
||||||
analysis_results += "\n3.结果分析: \n在此填写您的结果分析\n\n"
|
analysis_results += ("\n3.结果分析: \n"
|
||||||
|
f"{self.suggestion_template_list[2] if self.suggestion_template_list[2] is not None else '\n\n\n在此填写您的结果分析\n\n\n'}")
|
||||||
yield analysis_results
|
yield analysis_results
|
||||||
yield "改进措施"
|
yield "改进措施"
|
||||||
yield "注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n\n\n\n"
|
yield ("注:改进措施,包括课时分配、教材选用、教学方式、教学方法、教学内容、评分标准、过程评价及帮扶\n"
|
||||||
for i in range(88888):
|
f"{self.suggestion_template_list[1] if self.suggestion_template_list[1] is not None else '\n\n\n在这填入您的改进措施\n\n\n'}")
|
||||||
|
while True:
|
||||||
yield "如果您看到了本段文字,请联系开发者"
|
yield "如果您看到了本段文字,请联系开发者"
|
||||||
|
|
||||||
@staticmethod
|
def get_word_template_part_3(self):
|
||||||
def get_word_template_part_3():
|
|
||||||
yield "课程目标达成情况合理性评价"
|
yield "课程目标达成情况合理性评价"
|
||||||
yield "评价样本的合理性"
|
yield "评价样本的合理性"
|
||||||
yield "R全体样本 £抽样样本"
|
yield "R全体样本 £抽样样本"
|
||||||
yield "评价依据的合理性"
|
yield "评价依据的合理性"
|
||||||
yield "考核方法 R合适 £不合适 "
|
yield "考核方法 R合适 £不合适"
|
||||||
yield "考核内容是否支撑课程目标 R是 £否"
|
yield "考核内容是否支撑课程目标 R是 £否"
|
||||||
yield "评分标准 R明确 £不明确"
|
yield "评分标准 R明确 £不明确"
|
||||||
yield "计算过程的合理性"
|
yield "计算过程的合理性"
|
||||||
@@ -587,17 +676,23 @@ class ExcelReader:
|
|||||||
yield "R合理 £基本合理 £不合理"
|
yield "R合理 £基本合理 £不合理"
|
||||||
yield "专业负责人/系主任(签字)"
|
yield "专业负责人/系主任(签字)"
|
||||||
yield ("整改意见:\n"
|
yield ("整改意见:\n"
|
||||||
"\n\n\n\n\n"
|
|
||||||
"\n\n\n"
|
"\n\n\n"
|
||||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
f"{" " * 8}{self.suggestion_template_list[3] if self.suggestion_template_list[3] is not None else '\n\n\n'}\n\n\n")
|
||||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
yield ""
|
||||||
|
yield "签字:"
|
||||||
|
yield ""
|
||||||
|
yield "日期:"
|
||||||
|
yield datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
yield "课程负责人(签字)"
|
yield "课程负责人(签字)"
|
||||||
yield ("拟整改计划与措施:\n"
|
yield ("拟整改计划与措施:\n"
|
||||||
"\n\n\n\n\n"
|
|
||||||
"\n\n\n"
|
"\n\n\n"
|
||||||
"\n\t\t\t\t\t\t\t\t\t签字:\t\t\t日期:{}\n".
|
f"{" " * 8}{self.suggestion_template_list[4] if self.suggestion_template_list[4] is not None else '\n\n\n'}\n\n\n")
|
||||||
format(datetime.datetime.now().strftime("%Y-%m-%d")))
|
yield ""
|
||||||
for i in range(88888):
|
yield "签字:"
|
||||||
|
yield ""
|
||||||
|
yield "日期:"
|
||||||
|
yield datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
while True:
|
||||||
yield "如果您看到了本段文字,请联系开发者"
|
yield "如果您看到了本段文字,请联系开发者"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import pathlib
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from docx import Document
|
from docx import Document
|
||||||
|
from docx.enum.text import WD_BREAK
|
||||||
from docx.shared import Cm, Mm
|
from docx.shared import Cm, Mm
|
||||||
|
|
||||||
from module.schema import Course, Student
|
from module.schema import Course, Student
|
||||||
@@ -37,10 +38,11 @@ class DocPaper:
|
|||||||
section.right_margin = Cm(2)
|
section.right_margin = Cm(2)
|
||||||
|
|
||||||
def add_paper(self, course: Course, student: Student):
|
def add_paper(self, course: Course, student: Student):
|
||||||
temp_table = self._template.tables[0]
|
new_table = deepcopy(self._template.tables[0])
|
||||||
new_table = deepcopy(temp_table)
|
|
||||||
para = self._doc.add_paragraph()
|
para = self._doc.add_paragraph()
|
||||||
para._p.addprevious(new_table._element)
|
para._p.addprevious(new_table._element)
|
||||||
|
para.add_run().add_break(WD_BREAK.PAGE)
|
||||||
|
|
||||||
data_list = {
|
data_list = {
|
||||||
'%CNAME%': course.name,
|
'%CNAME%': course.name,
|
||||||
@@ -48,9 +50,7 @@ class DocPaper:
|
|||||||
'%SNAME%': student.name,
|
'%SNAME%': student.name,
|
||||||
'%NO%': student.no,
|
'%NO%': student.no,
|
||||||
'%SO%': student.so,
|
'%SO%': student.so,
|
||||||
'%Q1%': student.picked_questions[0].topic,
|
'%Q%': '\n'.join([f'\t{idx + 1}、{i.topic}' for idx, i in enumerate(student.picked_questions)])
|
||||||
'%Q2%': student.picked_questions[1].topic,
|
|
||||||
'%Q3%': student.picked_questions[2].topic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 替换表格中的占位符
|
# 替换表格中的占位符
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pytest
|
||||||
openpyxl~=3.1.5
|
openpyxl~=3.1.5
|
||||||
pyside6~=6.9.0
|
pyside6~=6.9.0
|
||||||
python-docx~=1.1.2
|
python-docx~=1.1.2
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
109
toolbox/config/achievement.default.excel.json
Normal file
109
toolbox/config/achievement.default.excel.json
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"name": "achievement",
|
||||||
|
"version": "9.0",
|
||||||
|
"compatibleVersion": [
|
||||||
|
"9.0"
|
||||||
|
],
|
||||||
|
"config": [
|
||||||
|
{
|
||||||
|
"name": "version",
|
||||||
|
"position": "H1",
|
||||||
|
"type": "single"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class_full_name",
|
||||||
|
"position": "D10",
|
||||||
|
"type": "single"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "course_name",
|
||||||
|
"position": "D5",
|
||||||
|
"type": "single"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teacher_name",
|
||||||
|
"position": "D7",
|
||||||
|
"type": "single"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "master_name",
|
||||||
|
"position": "D8",
|
||||||
|
"type": "single"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class_single_name",
|
||||||
|
"position": "K",
|
||||||
|
"type": "range",
|
||||||
|
"start": 2,
|
||||||
|
"end": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "class_single_number",
|
||||||
|
"position": "M",
|
||||||
|
"type": "range",
|
||||||
|
"start": 2,
|
||||||
|
"end": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "kpi_number",
|
||||||
|
"position": "H8",
|
||||||
|
"type": "single"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hml",
|
||||||
|
"position": "H",
|
||||||
|
"type": "range",
|
||||||
|
"start": 22,
|
||||||
|
"end": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hml_goal",
|
||||||
|
"position": "I",
|
||||||
|
"type": "range",
|
||||||
|
"start": 22,
|
||||||
|
"end": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hml_indicate",
|
||||||
|
"position": "Q",
|
||||||
|
"type": "range",
|
||||||
|
"start": 22,
|
||||||
|
"end": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
toolbox/config/override.excel.json
Normal file
0
toolbox/config/override.excel.json
Normal file
112
toolbox/models/config.py
Normal file
112
toolbox/models/config.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# 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 json
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from os import PathLike
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class BaseExcelConfig(ABC):
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def position(self) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class RangeExcelConfigMixin(ABC):
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def start(self) -> int:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def end(self) -> Optional[int]:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def fposition(self) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class SingleExcelConfigItem(BaseExcelConfig):
|
||||||
|
|
||||||
|
def __init__(self, name: str, position: str):
|
||||||
|
self._name = name
|
||||||
|
self._position = position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self) -> str:
|
||||||
|
return self._position
|
||||||
|
|
||||||
|
|
||||||
|
class RangeExcelConfigItem(SingleExcelConfigItem, RangeExcelConfigMixin):
|
||||||
|
|
||||||
|
def __init__(self, name: str, position: str, start: int, end: Optional[int] = None):
|
||||||
|
super().__init__(name, position)
|
||||||
|
self._start = start
|
||||||
|
self._end = end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self) -> int:
|
||||||
|
return self._start
|
||||||
|
|
||||||
|
@property
|
||||||
|
def end(self) -> Optional[int]:
|
||||||
|
return self._end
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fposition(self) -> str:
|
||||||
|
return self.position + '{}'
|
||||||
|
|
||||||
|
|
||||||
|
class AESConfig:
|
||||||
|
_config_list: list[SingleExcelConfigItem | RangeExcelConfigItem]
|
||||||
|
|
||||||
|
def __init__(self, file_path: str | PathLike):
|
||||||
|
self._file_path = file_path
|
||||||
|
self._config_list = []
|
||||||
|
self._init_config()
|
||||||
|
|
||||||
|
def __getitem__(self, item: str):
|
||||||
|
return self.get_config(item)
|
||||||
|
|
||||||
|
def _init_config(self):
|
||||||
|
with open(self._file_path, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
for item in config['config']:
|
||||||
|
itype = item.pop('type', None)
|
||||||
|
if itype == 'range':
|
||||||
|
self._config_list.append(RangeExcelConfigItem(**item))
|
||||||
|
elif itype == 'single':
|
||||||
|
self._config_list.append(SingleExcelConfigItem(**item))
|
||||||
|
|
||||||
|
def get_config(self, name: str) -> Optional[RangeExcelConfigItem | SingleExcelConfigItem]:
|
||||||
|
for config in self._config_list:
|
||||||
|
if config.name == name:
|
||||||
|
return config
|
||||||
|
return None
|
||||||
32
toolbox/models/data_model.py
Normal file
32
toolbox/models/data_model.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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/>.
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClassInfo:
|
||||||
|
full_name = ""
|
||||||
|
|
||||||
|
class_name: str
|
||||||
|
class_number: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CourseInfo:
|
||||||
|
course_name: str
|
||||||
|
# 任课教师
|
||||||
|
course_teacher_name: str
|
||||||
|
# 课程负责人
|
||||||
|
course_master_name: str
|
||||||
119
toolbox/services/excel_service.py
Normal file
119
toolbox/services/excel_service.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# 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 json
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from os import PathLike
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.workbook import Workbook
|
||||||
|
from openpyxl.worksheet.worksheet import Worksheet
|
||||||
|
|
||||||
|
from toolbox.models.data_model import ClassInfo
|
||||||
|
from toolbox.models.config import AESConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BaseExcelService(ABC):
|
||||||
|
_file_path: str | PathLike
|
||||||
|
_workbook: Optional[Workbook]
|
||||||
|
_sheet: None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def open(self, *args, **kwargs) -> 'BaseExcelService':
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save(self) -> 'BaseExcelService':
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def close(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def active_sheet(self, sheet_name: str) -> 'BaseExcelService':
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def cur_active_sheet(self) -> Worksheet:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class ExcelService(BaseExcelService):
|
||||||
|
def __init__(self, file_path: str | PathLike):
|
||||||
|
self._file_path = file_path
|
||||||
|
self._workbook = None
|
||||||
|
self._sheet = None
|
||||||
|
|
||||||
|
def open(self, *args, **kwargs):
|
||||||
|
self._workbook = openpyxl.load_workbook(self._file_path, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self._workbook.save(self._file_path)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._workbook.close()
|
||||||
|
|
||||||
|
def active_sheet(self, sheet_name: str):
|
||||||
|
self._sheet = self._workbook[sheet_name]
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cur_active_sheet(self):
|
||||||
|
return self._sheet
|
||||||
|
|
||||||
|
def load_value(self, cell: str):
|
||||||
|
if self._sheet is None:
|
||||||
|
raise ValueError("No active sheet. Please set an active sheet first.")
|
||||||
|
return self._sheet[cell].value
|
||||||
|
|
||||||
|
|
||||||
|
class AchievementExcelService(ExcelService):
|
||||||
|
version = ''
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
def __init__(self, file_path: str | PathLike):
|
||||||
|
super().__init__(file_path)
|
||||||
|
self.open(read_only=True, data_only=True)
|
||||||
|
|
||||||
|
def load_config(self, config_path: str | PathLike):
|
||||||
|
self.config = AESConfig(config_path)
|
||||||
|
|
||||||
|
def read_class_info(self) -> list[ClassInfo]:
|
||||||
|
lst = []
|
||||||
|
self.active_sheet('初始录入')
|
||||||
|
full_name = self.load_value(self.config['class_full_name'].position)
|
||||||
|
|
||||||
|
for i in range(self.config['class_single_name'].start, self.config['class_single_name'].end):
|
||||||
|
name = self.load_value(self.config['class_single_name'].fposition.format(i))
|
||||||
|
number = self.load_value(self.config['class_single_number'].fposition.format(i))
|
||||||
|
|
||||||
|
if name is None or number is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
ci = ClassInfo(name, number)
|
||||||
|
ci.full_name = full_name
|
||||||
|
lst.append(ci)
|
||||||
|
|
||||||
|
if len(lst) == 0:
|
||||||
|
raise ValueError("No class information found in the Excel file.")
|
||||||
|
|
||||||
|
return lst
|
||||||
|
|
||||||
|
def read_course_info(self):
|
||||||
|
...
|
||||||
19
toolbox/tests/__init__.py
Normal file
19
toolbox/tests/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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 os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TEST_FILE_PATH = Path(os.path.join(PACKAGE_DIR, 'files'))
|
||||||
41
toolbox/tests/test_config_model.py
Normal file
41
toolbox/tests/test_config_model.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 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/>.
|
||||||
|
from toolbox.models.config import AESConfig, SingleExcelConfigItem, RangeExcelConfigItem
|
||||||
|
from toolbox.tests import TEST_FILE_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_model():
|
||||||
|
aesc = AESConfig(TEST_FILE_PATH / 'test_config_model_01.json')
|
||||||
|
|
||||||
|
a = aesc.get_config('A')
|
||||||
|
assert isinstance(a, SingleExcelConfigItem)
|
||||||
|
assert a.position == 'H1'
|
||||||
|
|
||||||
|
b = aesc.get_config('B')
|
||||||
|
assert isinstance(b, SingleExcelConfigItem)
|
||||||
|
assert b.position == 'D10'
|
||||||
|
|
||||||
|
c = aesc.get_config('C')
|
||||||
|
assert isinstance(c, RangeExcelConfigItem)
|
||||||
|
assert c.position == 'K'
|
||||||
|
assert c.fposition.format(1) == 'K1'
|
||||||
|
assert c.start == 2
|
||||||
|
assert c.end == 5
|
||||||
|
|
||||||
|
d = aesc.get_config('D')
|
||||||
|
assert isinstance(d, RangeExcelConfigItem)
|
||||||
|
assert d.position == 'Q'
|
||||||
|
assert d.fposition.format(d.start) == 'Q22'
|
||||||
|
assert d.end is None
|
||||||
79
toolbox/tests/test_excel_services.py
Normal file
79
toolbox/tests/test_excel_services.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# 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 shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from openpyxl.workbook import Workbook
|
||||||
|
|
||||||
|
from toolbox.services.excel_service import ExcelService, AchievementExcelService
|
||||||
|
from toolbox.tests import TEST_FILE_PATH
|
||||||
|
|
||||||
|
SAVE_TEMP_FILE = False
|
||||||
|
|
||||||
|
|
||||||
|
class TestExcelService:
|
||||||
|
es = ExcelService(TEST_FILE_PATH / 'test_excel_services_01.xlsx')
|
||||||
|
es.open(data_only=True)
|
||||||
|
|
||||||
|
def test_open(self):
|
||||||
|
assert isinstance(self.es._workbook, Workbook)
|
||||||
|
assert self.es._sheet is None
|
||||||
|
|
||||||
|
def test_open_failed(self):
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
ExcelService(TEST_FILE_PATH / 'non_existent_file.xlsx').open()
|
||||||
|
|
||||||
|
def test_active_sheet(self):
|
||||||
|
self.es.active_sheet('Sheet1')
|
||||||
|
assert self.es._sheet.title == 'Sheet1'
|
||||||
|
self.es.active_sheet('Sheet2')
|
||||||
|
assert self.es._sheet.title == 'Sheet2'
|
||||||
|
|
||||||
|
def test_cur_active_sheet(self):
|
||||||
|
self.es.active_sheet('Sheet1')
|
||||||
|
assert self.es.cur_active_sheet.title == 'Sheet1'
|
||||||
|
self.es.active_sheet('Sheet2')
|
||||||
|
assert self.es.cur_active_sheet.title == 'Sheet2'
|
||||||
|
|
||||||
|
def test_save_and_close(self):
|
||||||
|
temp_excel_file = TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx'
|
||||||
|
shutil.copy(self.es._file_path, temp_excel_file)
|
||||||
|
|
||||||
|
self.es.active_sheet('Sheet1').cur_active_sheet['A1'] = 'Modified'
|
||||||
|
self.es.save().close()
|
||||||
|
|
||||||
|
es2 = ExcelService(temp_excel_file).open().active_sheet('Sheet1')
|
||||||
|
assert es2.cur_active_sheet['A1'].value == 'Modified'
|
||||||
|
es2.close()
|
||||||
|
|
||||||
|
if not SAVE_TEMP_FILE:
|
||||||
|
Path(TEST_FILE_PATH / 'test_excel_services_01_temp.xlsx').unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAchievementExcelService:
|
||||||
|
aes = AchievementExcelService(TEST_FILE_PATH / 'test_achievement_excel_service_01.xlsm')
|
||||||
|
aes.load_config(TEST_FILE_PATH / 'test_achievement.default.excel_01.json')
|
||||||
|
|
||||||
|
def test_read_class_info(self):
|
||||||
|
cis = self.aes.read_class_info()
|
||||||
|
assert len(cis) == 2
|
||||||
|
assert cis[0].full_name == '22工程管理(1)(2)'
|
||||||
|
assert cis[1].full_name == '22工程管理(1)(2)'
|
||||||
|
|
||||||
|
assert cis[0].class_name == '22工程管理(1)'
|
||||||
|
assert cis[0].class_number == 34
|
||||||
|
assert cis[1].class_name == '22工程管理(2)'
|
||||||
|
assert cis[1].class_number == 36
|
||||||
@@ -109,3 +109,12 @@ class MyGroupHeaderCardWidget(GroupHeaderCardWidget):
|
|||||||
self.groupLayout.addWidget(group)
|
self.groupLayout.addWidget(group)
|
||||||
self.groupWidgets.append(group)
|
self.groupWidgets.append(group)
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
class NotImplementedWidget(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.label = DisplayLabel("🚧", self)
|
||||||
|
self.label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.layout.addWidget(self.label)
|
||||||
|
|||||||
99
ui/main.py
99
ui/main.py
@@ -13,6 +13,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from PySide6.QtGui import QIcon, QShowEvent
|
from PySide6.QtGui import QIcon, QShowEvent
|
||||||
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor
|
from qfluentwidgets import FluentIcon, MSFluentWindow, NavigationItemPosition, MessageBox, setThemeColor
|
||||||
|
|
||||||
@@ -25,35 +29,94 @@ from ui.pyui.test_ui import TestWidget
|
|||||||
from utils.function import RELEASE_ENV
|
from utils.function import RELEASE_ENV
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InterfaceSpec:
|
||||||
|
key: str
|
||||||
|
factory: Callable[[], object]
|
||||||
|
icon: FluentIcon
|
||||||
|
nav_text: str
|
||||||
|
position: NavigationItemPosition = NavigationItemPosition.TOP
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(MSFluentWindow):
|
class MainWindow(MSFluentWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
setThemeColor(MAIN_THEME_COLOR)
|
setThemeColor(MAIN_THEME_COLOR)
|
||||||
self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR)
|
self.setCustomBackgroundColor(BLUE_BACKGROUND_COLOR, BLUE_BACKGROUND_COLOR)
|
||||||
|
self.interface_specs = self.build_interface_specs()
|
||||||
self.achievementInterface = AchievementWidget('Achievement Interface', self)
|
self.interfaces = self.create_interfaces(self.interface_specs)
|
||||||
self.defenseInterface = DefenseWidget('Defense Interface', self)
|
self.bind_error_handlers()
|
||||||
self.aboutInterface = AboutWidget('About Interface', self)
|
|
||||||
self.pickerInterface = PickerWidget('Picker Interface', self)
|
|
||||||
if not RELEASE_ENV:
|
|
||||||
self.testInterface = TestWidget('Test Interface', self)
|
|
||||||
|
|
||||||
self.achievementInterface.error.connect(self.showError)
|
|
||||||
self.defenseInterface.errorSignal.connect(self.showError)
|
|
||||||
self.pickerInterface.errorSignal.connect(self.showError)
|
|
||||||
|
|
||||||
self.initNavigation()
|
self.initNavigation()
|
||||||
self.initWindow()
|
self.initWindow()
|
||||||
|
|
||||||
def initNavigation(self):
|
def build_interface_specs(self) -> list[InterfaceSpec]:
|
||||||
self.addSubInterface(self.achievementInterface, FluentIcon.SPEED_HIGH, '达成度')
|
return [
|
||||||
self.addSubInterface(self.defenseInterface, FluentIcon.FEEDBACK, '答辩题目')
|
InterfaceSpec(
|
||||||
self.addSubInterface(self.pickerInterface, FluentIcon.PEOPLE, '提问')
|
key="achievement",
|
||||||
if not RELEASE_ENV:
|
factory=lambda: AchievementWidget('Achievement Interface', self),
|
||||||
self.addSubInterface(self.testInterface, FluentIcon.VIEW, '测试')
|
icon=FluentIcon.SPEED_HIGH,
|
||||||
|
nav_text='达成度',
|
||||||
|
enabled=True,
|
||||||
|
),
|
||||||
|
InterfaceSpec(
|
||||||
|
key="defense",
|
||||||
|
factory=lambda: DefenseWidget('Defense Interface', self),
|
||||||
|
icon=FluentIcon.FEEDBACK,
|
||||||
|
nav_text='答辩',
|
||||||
|
enabled=True,
|
||||||
|
),
|
||||||
|
InterfaceSpec(
|
||||||
|
key="picker",
|
||||||
|
factory=lambda: PickerWidget('Picker Interface', self),
|
||||||
|
icon=FluentIcon.PEOPLE,
|
||||||
|
nav_text='提问',
|
||||||
|
enabled=not RELEASE_ENV,
|
||||||
|
),
|
||||||
|
InterfaceSpec(
|
||||||
|
key="test",
|
||||||
|
factory=lambda: TestWidget('Test Interface', self),
|
||||||
|
icon=FluentIcon.VIEW,
|
||||||
|
nav_text='测试',
|
||||||
|
enabled=not RELEASE_ENV,
|
||||||
|
),
|
||||||
|
InterfaceSpec(
|
||||||
|
key="about",
|
||||||
|
factory=lambda: AboutWidget('About Interface', self),
|
||||||
|
icon=FluentIcon.INFO,
|
||||||
|
nav_text='关于',
|
||||||
|
position=NavigationItemPosition.BOTTOM,
|
||||||
|
enabled=True
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
self.addSubInterface(self.aboutInterface, FluentIcon.INFO, '关于', position=NavigationItemPosition.BOTTOM)
|
def create_interfaces(self, specs: list[InterfaceSpec]) -> dict[str, object]:
|
||||||
|
interfaces: dict[str, object] = {}
|
||||||
|
for spec in specs:
|
||||||
|
if not spec.enabled:
|
||||||
|
continue
|
||||||
|
widget = spec.factory()
|
||||||
|
interfaces[spec.key] = widget
|
||||||
|
setattr(self, f"{spec.key}Interface", widget)
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
def bind_error_handlers(self):
|
||||||
|
achievement = self.interfaces.get("achievement")
|
||||||
|
defense = self.interfaces.get("defense")
|
||||||
|
|
||||||
|
if achievement and hasattr(achievement, "error"):
|
||||||
|
achievement.error.connect(self.showError)
|
||||||
|
if defense and hasattr(defense, "errorSignal"):
|
||||||
|
defense.errorSignal.connect(self.showError)
|
||||||
|
|
||||||
|
def initNavigation(self):
|
||||||
|
for spec in self.interface_specs:
|
||||||
|
widget = self.interfaces.get(spec.key)
|
||||||
|
if not widget:
|
||||||
|
continue
|
||||||
|
self.addSubInterface(widget, spec.icon, spec.nav_text, position=spec.position)
|
||||||
|
|
||||||
def initWindow(self):
|
def initWindow(self):
|
||||||
self.resize(900, 700)
|
self.resize(900, 700)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2025 Jeffrey Hsu - JITToolBox
|
# Copyright (c) 2025-2026 Jeffrey Hsu - JITToolBox
|
||||||
# #
|
# #
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -20,7 +20,7 @@ from typing import Callable, Literal
|
|||||||
from PySide6.QtCore import Qt, Signal, QThread
|
from PySide6.QtCore import Qt, Signal, QThread
|
||||||
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout
|
from PySide6.QtWidgets import QVBoxLayout, QFileDialog, QHBoxLayout
|
||||||
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \
|
from qfluentwidgets import GroupHeaderCardWidget, FluentIcon, PushButton, LineEdit, IconWidget, BodyLabel, \
|
||||||
PrimaryPushButton, SwitchButton, HyperlinkButton
|
PrimaryPushButton, SwitchButton, HyperlinkButton, InfoBar, InfoBarPosition
|
||||||
|
|
||||||
from module import LOGLEVEL
|
from module import LOGLEVEL
|
||||||
from module.worker import ARGWorker
|
from module.worker import ARGWorker
|
||||||
@@ -40,7 +40,7 @@ class InputSettingCard(MyGroupHeaderCardWidget):
|
|||||||
self.setBorderRadius(8)
|
self.setBorderRadius(8)
|
||||||
self.btnHBoxLayout = QHBoxLayout(self)
|
self.btnHBoxLayout = QHBoxLayout(self)
|
||||||
|
|
||||||
self.openTemplateButton = HyperlinkButton("", "下载模板")
|
self.openTemplateButton = HyperlinkButton("", "模板下载")
|
||||||
self.chooseFileButton = PushButton("打开")
|
self.chooseFileButton = PushButton("打开")
|
||||||
|
|
||||||
self.chooseFileButton.setFixedWidth(120)
|
self.chooseFileButton.setFixedWidth(120)
|
||||||
@@ -259,3 +259,23 @@ class AchievementWidget(Widget):
|
|||||||
def show_info(self, content: str, level: str):
|
def show_info(self, content: str, level: str):
|
||||||
if level == LOGLEVEL.INFO:
|
if level == LOGLEVEL.INFO:
|
||||||
self.pib.set_title(content)
|
self.pib.set_title(content)
|
||||||
|
elif level == LOGLEVEL.WARNING:
|
||||||
|
InfoBar.warning(
|
||||||
|
title='提示',
|
||||||
|
content=content,
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
|
duration=5000,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
elif level == LOGLEVEL.ERROR:
|
||||||
|
InfoBar.error(
|
||||||
|
title='错误',
|
||||||
|
content=content,
|
||||||
|
orient=Qt.Horizontal,
|
||||||
|
isClosable=True,
|
||||||
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
|
duration=-1,
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, BodyLa
|
|||||||
from module.worker import DTGWorker
|
from module.worker import DTGWorker
|
||||||
from ui import MAIN_THEME_COLOR
|
from ui import MAIN_THEME_COLOR
|
||||||
from ui.components.infobar import ProgressInfoBar
|
from ui.components.infobar import ProgressInfoBar
|
||||||
from ui.components.widget import Widget, MyGroupHeaderCardWidget
|
from ui.components.widget import Widget, MyGroupHeaderCardWidget, NotImplementedWidget
|
||||||
from ui.pyui.sub.defense import ODModeExportSettings, ODModeSettings
|
from ui.pyui.sub.defense import ODModeExportSettings, ODModeSettings
|
||||||
from utils.function import open_template
|
from utils.function import open_template
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ class InitSettingCard(MyGroupHeaderCardWidget):
|
|||||||
|
|
||||||
self.sBtnHBoxLayout = QHBoxLayout(self)
|
self.sBtnHBoxLayout = QHBoxLayout(self)
|
||||||
self.qBtnHBoxLayout = QHBoxLayout(self)
|
self.qBtnHBoxLayout = QHBoxLayout(self)
|
||||||
self.sTemplateButton = HyperlinkButton("", "下载模板")
|
self.sTemplateButton = HyperlinkButton("", "模板下载")
|
||||||
self.chooseStudentButton = PushButton("打开")
|
self.chooseStudentButton = PushButton("打开")
|
||||||
self.qTemplateButton = HyperlinkButton("", "下载模板")
|
self.qTemplateButton = HyperlinkButton("", "模板下载")
|
||||||
self.chooseQuestionButton = PushButton("打开")
|
self.chooseQuestionButton = PushButton("打开")
|
||||||
|
|
||||||
self.chooseStudentButton.setFixedWidth(120)
|
self.chooseStudentButton.setFixedWidth(120)
|
||||||
@@ -136,7 +136,7 @@ class ExportSettingsCard(GroupHeaderCardWidget):
|
|||||||
self.radioGroup.addButton(self.wordRadio)
|
self.radioGroup.addButton(self.wordRadio)
|
||||||
self.radioHbox.addWidget(self.pdfRadio)
|
self.radioHbox.addWidget(self.pdfRadio)
|
||||||
self.radioHbox.addWidget(self.wordRadio)
|
self.radioHbox.addWidget(self.wordRadio)
|
||||||
self.pdfRadio.setChecked(True)
|
self.wordRadio.setChecked(True)
|
||||||
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR))
|
||||||
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉")
|
||||||
self.chooseExportDirectoryButton.setFixedWidth(120)
|
self.chooseExportDirectoryButton.setFixedWidth(120)
|
||||||
@@ -345,7 +345,7 @@ class DefenseWidget(Widget):
|
|||||||
self.stack = QStackedWidget(self)
|
self.stack = QStackedWidget(self)
|
||||||
self.menu = SegmentedWidget(self)
|
self.menu = SegmentedWidget(self)
|
||||||
self.dpMode = DPMode(self)
|
self.dpMode = DPMode(self)
|
||||||
self.doMode = DOMode(self)
|
self.doMode = NotImplementedWidget(self)
|
||||||
|
|
||||||
self.addSubInterface(self.dpMode, 'DPMode', '书面答辩')
|
self.addSubInterface(self.dpMode, 'DPMode', '书面答辩')
|
||||||
self.addSubInterface(self.doMode, 'DOMode', '口头答辩')
|
self.addSubInterface(self.doMode, 'DOMode', '口头答辩')
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class PickStudentMode(QWidget):
|
|||||||
self.vbox.setContentsMargins(0, 0, 0, 0)
|
self.vbox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.btnHBox = QHBoxLayout(self)
|
self.btnHBox = QHBoxLayout(self)
|
||||||
|
|
||||||
self.openTemplateBtn = HyperlinkButton("", "下载模板")
|
self.openTemplateBtn = HyperlinkButton("", "模板下载")
|
||||||
self.chooseBtn = PushButton("打开")
|
self.chooseBtn = PushButton("打开")
|
||||||
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
|
self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始")
|
||||||
self.bottomLayout = QHBoxLayout()
|
self.bottomLayout = QHBoxLayout()
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ class ODModeSettings(MyGroupHeaderCardWidget):
|
|||||||
self.setTitle('输入选项')
|
self.setTitle('输入选项')
|
||||||
|
|
||||||
self.sBtn = PushButton('打开', self)
|
self.sBtn = PushButton('打开', self)
|
||||||
self.sBtnTemplate = HyperlinkButton('', '下载模板')
|
self.sBtnTemplate = HyperlinkButton('', '模板下载')
|
||||||
|
|
||||||
self.qBtn = PushButton('打开', self)
|
self.qBtn = PushButton('打开', self)
|
||||||
self.bBtnTemplate = HyperlinkButton('', '下载模板')
|
self.bBtnTemplate = HyperlinkButton('', '模板下载')
|
||||||
|
|
||||||
self.qNumber = SpinBox(self)
|
self.qNumber = SpinBox(self)
|
||||||
self.qNumber.setRange(0, 999)
|
self.qNumber.setRange(0, 999)
|
||||||
|
|||||||
@@ -18,11 +18,23 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
def gen_build_info():
|
def gen_build_info():
|
||||||
hash_str = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('utf-8').strip()
|
try:
|
||||||
|
hash_str = subprocess.check_output(
|
||||||
|
['git', 'rev-parse', '--short', 'HEAD'],
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
).decode('utf-8').strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
# git 未安装
|
||||||
|
hash_str = 'unknown'
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
# 不是 git 仓库(如从压缩包下载)
|
||||||
|
hash_str = 'unknown'
|
||||||
|
|
||||||
with open('build_info.py', 'w', encoding='utf-8') as f:
|
with open('build_info.py', 'w', encoding='utf-8') as f:
|
||||||
f.write(f"# Auto-generated build info\n")
|
f.write(f"# Auto-generated build info\n")
|
||||||
f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n")
|
f.write(f"BUILD_TIME = '{datetime.now().isoformat(sep=' ', timespec='seconds')}'\n")
|
||||||
f.write(f"GIT_HASH = '{hash_str}'\n")
|
f.write(f"GIT_HASH = '{hash_str}'\n")
|
||||||
|
|
||||||
|
|
||||||
gen_build_info()
|
if __name__ == '__main__':
|
||||||
|
gen_build_info()
|
||||||
|
|||||||
Reference in New Issue
Block a user