add result sorting controls
This commit is contained in:
@@ -33,7 +33,7 @@ V2:zip 解压后包含 `Patients_info.csv`,并按患者目录分别保存检
|
|||||||
|
|
||||||
患者编号类型可选择自动识别。自动识别会读取 `Patients_info.csv` 中的 `pat_no`,并与 `Tests_List` 文件名或患者目录名比对:若更匹配 10 位补零编号,则使用 `pat_no`;若更匹配原始编号,则使用 `zhuyuanhao`。
|
患者编号类型可选择自动识别。自动识别会读取 `Patients_info.csv` 中的 `pat_no`,并与 `Tests_List` 文件名或患者目录名比对:若更匹配 10 位补零编号,则使用 `pat_no`;若更匹配原始编号,则使用 `zhuyuanhao`。
|
||||||
|
|
||||||
系统默认输出完整结果;处理完成后可在结果页通过“内容选择”立即切换预览内容,并同步影响右侧“导出此 Excel”的文件内容。可选择是否保留:
|
系统默认输出完整结果;处理完成后可在结果页通过“内容选择”立即切换预览内容,并同步影响右侧“导出此 Excel”的文件内容。结果页还可以按姓名、采样时间或检测原因选择升序/降序排序;“未检测到内容汇总”会在每个分组标题下单独排序,不会把不同检测原因的分组混在一起。可选择是否保留:
|
||||||
|
|
||||||
- 基本工作表
|
- 基本工作表
|
||||||
- 未匹配检测内容项
|
- 未匹配检测内容项
|
||||||
|
|||||||
64
app/main.py
64
app/main.py
@@ -23,6 +23,15 @@ WORK_ROOT.mkdir(parents=True, exist_ok=True)
|
|||||||
|
|
||||||
APP_TITLE = "\u68c0\u6d4b\u6570\u636e\u5904\u7406"
|
APP_TITLE = "\u68c0\u6d4b\u6570\u636e\u5904\u7406"
|
||||||
UNMATCHED = "\u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9"
|
UNMATCHED = "\u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9"
|
||||||
|
SORT_OPTIONS = {
|
||||||
|
"name": "\u59d3\u540d",
|
||||||
|
"sample_time": "\u91c7\u6837\u65f6\u95f4",
|
||||||
|
"reason": "\u68c0\u6d4b\u539f\u56e0",
|
||||||
|
}
|
||||||
|
SORT_ORDERS = {
|
||||||
|
"asc": "\u5347\u5e8f",
|
||||||
|
"desc": "\u964d\u5e8f",
|
||||||
|
}
|
||||||
|
|
||||||
app = FastAPI(title=APP_TITLE)
|
app = FastAPI(title=APP_TITLE)
|
||||||
|
|
||||||
@@ -117,16 +126,22 @@ def result_page(
|
|||||||
include_basic_sheets: bool = Query(True),
|
include_basic_sheets: bool = Query(True),
|
||||||
include_unmatched_items: bool = Query(True),
|
include_unmatched_items: bool = Query(True),
|
||||||
include_summary_sheet: bool = Query(True),
|
include_summary_sheet: bool = Query(True),
|
||||||
|
sort_by: str = Query("sample_time"),
|
||||||
|
sort_order: str = Query("asc"),
|
||||||
) -> str:
|
) -> str:
|
||||||
job_dir = _get_job_dir(job_id)
|
job_dir = _get_job_dir(job_id)
|
||||||
try:
|
try:
|
||||||
rows = _clean_preview_rows(preview_rows)
|
rows = _clean_preview_rows(preview_rows)
|
||||||
|
clean_sort_by = _clean_sort_by(sort_by)
|
||||||
|
clean_sort_order = _clean_sort_order(sort_order)
|
||||||
result = summarize_job(
|
result = summarize_job(
|
||||||
job_dir,
|
job_dir,
|
||||||
rows,
|
rows,
|
||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=clean_sort_by,
|
||||||
|
sort_order=clean_sort_order,
|
||||||
)
|
)
|
||||||
except ProcessingError as exc:
|
except ProcessingError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
@@ -136,6 +151,8 @@ def result_page(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=clean_sort_by,
|
||||||
|
sort_order=clean_sort_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -145,6 +162,8 @@ def download_all(
|
|||||||
include_basic_sheets: bool = Query(True),
|
include_basic_sheets: bool = Query(True),
|
||||||
include_unmatched_items: bool = Query(True),
|
include_unmatched_items: bool = Query(True),
|
||||||
include_summary_sheet: bool = Query(True),
|
include_summary_sheet: bool = Query(True),
|
||||||
|
sort_by: str = Query("sample_time"),
|
||||||
|
sort_order: str = Query("asc"),
|
||||||
) -> FileResponse:
|
) -> FileResponse:
|
||||||
job_dir = _get_job_dir(job_id)
|
job_dir = _get_job_dir(job_id)
|
||||||
try:
|
try:
|
||||||
@@ -153,6 +172,8 @@ def download_all(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=_clean_sort_by(sort_by),
|
||||||
|
sort_order=_clean_sort_order(sort_order),
|
||||||
)
|
)
|
||||||
except ProcessingError as exc:
|
except ProcessingError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
@@ -166,6 +187,8 @@ def download_file(
|
|||||||
include_basic_sheets: bool = Query(True),
|
include_basic_sheets: bool = Query(True),
|
||||||
include_unmatched_items: bool = Query(True),
|
include_unmatched_items: bool = Query(True),
|
||||||
include_summary_sheet: bool = Query(True),
|
include_summary_sheet: bool = Query(True),
|
||||||
|
sort_by: str = Query("sample_time"),
|
||||||
|
sort_order: str = Query("asc"),
|
||||||
) -> FileResponse:
|
) -> FileResponse:
|
||||||
job_dir = _get_job_dir(job_id)
|
job_dir = _get_job_dir(job_id)
|
||||||
try:
|
try:
|
||||||
@@ -175,6 +198,8 @@ def download_file(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=_clean_sort_by(sort_by),
|
||||||
|
sort_order=_clean_sort_order(sort_order),
|
||||||
)
|
)
|
||||||
except ProcessingError as exc:
|
except ProcessingError as exc:
|
||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
@@ -207,12 +232,26 @@ def _checked(value: bool) -> str:
|
|||||||
return "checked" if value else ""
|
return "checked" if value else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _selected(value: str, current: str) -> str:
|
||||||
|
return "selected" if value == current else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_sort_by(sort_by: str) -> str:
|
||||||
|
return sort_by if sort_by in SORT_OPTIONS else "sample_time"
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_sort_order(sort_order: str) -> str:
|
||||||
|
return sort_order if sort_order in SORT_ORDERS else "asc"
|
||||||
|
|
||||||
|
|
||||||
def _render_result(
|
def _render_result(
|
||||||
result: ProcessingResult,
|
result: ProcessingResult,
|
||||||
preview_rows: int = 20,
|
preview_rows: int = 20,
|
||||||
include_basic_sheets: bool = True,
|
include_basic_sheets: bool = True,
|
||||||
include_unmatched_items: bool = True,
|
include_unmatched_items: bool = True,
|
||||||
include_summary_sheet: bool = True,
|
include_summary_sheet: bool = True,
|
||||||
|
sort_by: str = "sample_time",
|
||||||
|
sort_order: str = "asc",
|
||||||
) -> str:
|
) -> str:
|
||||||
total_sheets = sum(len(file.sheets) for file in result.files)
|
total_sheets = sum(len(file.sheets) for file in result.files)
|
||||||
total_rows = sum(max(sheet.rows - 1, 0) for file in result.files for sheet in file.sheets)
|
total_rows = sum(max(sheet.rows - 1, 0) for file in result.files for sheet in file.sheets)
|
||||||
@@ -230,6 +269,16 @@ def _render_result(
|
|||||||
<label><input class="export-option" type="checkbox" data-param="include_basic_sheets" {_checked(include_basic_sheets)}> \u57fa\u672c\u5de5\u4f5c\u8868</label>
|
<label><input class="export-option" type="checkbox" data-param="include_basic_sheets" {_checked(include_basic_sheets)}> \u57fa\u672c\u5de5\u4f5c\u8868</label>
|
||||||
<label><input class="export-option" type="checkbox" data-param="include_unmatched_items" {_checked(include_unmatched_items)}> \u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9\u9879</label>
|
<label><input class="export-option" type="checkbox" data-param="include_unmatched_items" {_checked(include_unmatched_items)}> \u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9\u9879</label>
|
||||||
<label><input class="export-option" type="checkbox" data-param="include_summary_sheet" {_checked(include_summary_sheet)}> \u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868</label>
|
<label><input class="export-option" type="checkbox" data-param="include_summary_sheet" {_checked(include_summary_sheet)}> \u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868</label>
|
||||||
|
<label for="sort_by">\u6392\u5e8f</label>
|
||||||
|
<select id="sort_by" class="result-option" data-param="sort_by">
|
||||||
|
<option value="name" {_selected("name", sort_by)}>\u6309\u59d3\u540d</option>
|
||||||
|
<option value="sample_time" {_selected("sample_time", sort_by)}>\u6309\u91c7\u6837\u65f6\u95f4</option>
|
||||||
|
<option value="reason" {_selected("reason", sort_by)}>\u6309\u68c0\u6d4b\u539f\u56e0</option>
|
||||||
|
</select>
|
||||||
|
<select id="sort_order" class="result-option" data-param="sort_order">
|
||||||
|
<option value="asc" {_selected("asc", sort_order)}>\u5347\u5e8f</option>
|
||||||
|
<option value="desc" {_selected("desc", sort_order)}>\u964d\u5e8f</option>
|
||||||
|
</select>
|
||||||
</section>
|
</section>
|
||||||
<form class="row-control" method="get" action="/result/{html.escape(result.job_id)}">
|
<form class="row-control" method="get" action="/result/{html.escape(result.job_id)}">
|
||||||
<label for="preview_rows">\u6bcf\u4e2a\u5de5\u4f5c\u8868\u9884\u89c8\u884c\u6570</label>
|
<label for="preview_rows">\u6bcf\u4e2a\u5de5\u4f5c\u8868\u9884\u89c8\u884c\u6570</label>
|
||||||
@@ -411,6 +460,11 @@ def _page_shell(
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}}
|
}}
|
||||||
|
.export-options select {{
|
||||||
|
width: auto;
|
||||||
|
min-width: 128px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}}
|
||||||
.checks label {{
|
.checks label {{
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -591,6 +645,9 @@ def _page_shell(
|
|||||||
const options = Array.from(document.querySelectorAll('.export-option'));
|
const options = Array.from(document.querySelectorAll('.export-option'));
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
options.forEach((option) => params.set(option.dataset.param, option.checked ? 'true' : 'false'));
|
options.forEach((option) => params.set(option.dataset.param, option.checked ? 'true' : 'false'));
|
||||||
|
document.querySelectorAll('.result-option').forEach((option) => {{
|
||||||
|
params.set(option.dataset.param, option.value);
|
||||||
|
}});
|
||||||
document.querySelectorAll('.export-link').forEach((link) => {{
|
document.querySelectorAll('.export-link').forEach((link) => {{
|
||||||
const base = link.dataset.base;
|
const base = link.dataset.base;
|
||||||
const sep = base.includes('?') ? '&' : '?';
|
const sep = base.includes('?') ? '&' : '?';
|
||||||
@@ -606,6 +663,9 @@ def _page_shell(
|
|||||||
document.querySelectorAll('.export-option').forEach((option) => {{
|
document.querySelectorAll('.export-option').forEach((option) => {{
|
||||||
url.searchParams.set(option.dataset.param, option.checked ? 'true' : 'false');
|
url.searchParams.set(option.dataset.param, option.checked ? 'true' : 'false');
|
||||||
}});
|
}});
|
||||||
|
document.querySelectorAll('.result-option').forEach((option) => {{
|
||||||
|
url.searchParams.set(option.dataset.param, option.value);
|
||||||
|
}});
|
||||||
window.location.href = url.toString();
|
window.location.href = url.toString();
|
||||||
}}
|
}}
|
||||||
const resultForm = document.querySelector('.row-control');
|
const resultForm = document.querySelector('.row-control');
|
||||||
@@ -619,6 +679,10 @@ def _page_shell(
|
|||||||
updateExportLinks();
|
updateExportLinks();
|
||||||
refreshResultWithOptions();
|
refreshResultWithOptions();
|
||||||
}}));
|
}}));
|
||||||
|
document.querySelectorAll('.result-option').forEach((option) => option.addEventListener('change', () => {{
|
||||||
|
updateExportLinks();
|
||||||
|
refreshResultWithOptions();
|
||||||
|
}}));
|
||||||
updateExportLinks();
|
updateExportLinks();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
153
app/processor.py
153
app/processor.py
@@ -6,6 +6,7 @@ import sys
|
|||||||
import uuid
|
import uuid
|
||||||
import zipfile
|
import zipfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
@@ -17,6 +18,8 @@ PROCESSOR_DIR = Path(__file__).resolve().parent / "processors"
|
|||||||
SUMMARY_SHEET_NAME = "\u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b"
|
SUMMARY_SHEET_NAME = "\u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b"
|
||||||
UNMATCHED_HEADER = "\u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9"
|
UNMATCHED_HEADER = "\u672a\u5339\u914d\u68c0\u6d4b\u5185\u5bb9"
|
||||||
UNMATCHED_FILL = PatternFill(fill_type="solid", fgColor="FCE4D6")
|
UNMATCHED_FILL = PatternFill(fill_type="solid", fgColor="FCE4D6")
|
||||||
|
SORT_FIELDS = {"none", "name", "sample_time", "reason"}
|
||||||
|
SORT_ORDERS = {"asc", "desc"}
|
||||||
|
|
||||||
|
|
||||||
class ProcessingError(Exception):
|
class ProcessingError(Exception):
|
||||||
@@ -59,6 +62,8 @@ def run_processing(
|
|||||||
include_basic_sheets: bool = True,
|
include_basic_sheets: bool = True,
|
||||||
include_unmatched_items: bool = True,
|
include_unmatched_items: bool = True,
|
||||||
include_summary_sheet: bool = True,
|
include_summary_sheet: bool = True,
|
||||||
|
sort_by: str = "sample_time",
|
||||||
|
sort_order: str = "asc",
|
||||||
) -> ProcessingResult:
|
) -> ProcessingResult:
|
||||||
if mode not in {"auto", "v1", "v2"}:
|
if mode not in {"auto", "v1", "v2"}:
|
||||||
raise ProcessingError("处理模式不正确。")
|
raise ProcessingError("处理模式不正确。")
|
||||||
@@ -142,6 +147,8 @@ def run_processing(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
result_zip = job_dir / "result.zip"
|
result_zip = job_dir / "result.zip"
|
||||||
@@ -160,6 +167,8 @@ def create_result_zip(
|
|||||||
include_basic_sheets: bool = True,
|
include_basic_sheets: bool = True,
|
||||||
include_unmatched_items: bool = True,
|
include_unmatched_items: bool = True,
|
||||||
include_summary_sheet: bool = True,
|
include_summary_sheet: bool = True,
|
||||||
|
sort_by: str = "sample_time",
|
||||||
|
sort_order: str = "asc",
|
||||||
) -> Path:
|
) -> Path:
|
||||||
output_dir = job_dir / "output"
|
output_dir = job_dir / "output"
|
||||||
result_zip = job_dir / "result.zip"
|
result_zip = job_dir / "result.zip"
|
||||||
@@ -176,6 +185,8 @@ def create_result_zip(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
result_zip = export_dir / "result.zip"
|
result_zip = export_dir / "result.zip"
|
||||||
@@ -189,6 +200,8 @@ def summarize_job(
|
|||||||
include_basic_sheets: bool = True,
|
include_basic_sheets: bool = True,
|
||||||
include_unmatched_items: bool = True,
|
include_unmatched_items: bool = True,
|
||||||
include_summary_sheet: bool = True,
|
include_summary_sheet: bool = True,
|
||||||
|
sort_by: str = "sample_time",
|
||||||
|
sort_order: str = "asc",
|
||||||
) -> ProcessingResult:
|
) -> ProcessingResult:
|
||||||
output_dir = job_dir / "output"
|
output_dir = job_dir / "output"
|
||||||
if not output_dir.exists():
|
if not output_dir.exists():
|
||||||
@@ -204,6 +217,8 @@ def summarize_job(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
xlsx_files = sorted(preview_dir.rglob("*.xlsx"))
|
xlsx_files = sorted(preview_dir.rglob("*.xlsx"))
|
||||||
@@ -226,6 +241,8 @@ def find_output_file(
|
|||||||
include_basic_sheets: bool = True,
|
include_basic_sheets: bool = True,
|
||||||
include_unmatched_items: bool = True,
|
include_unmatched_items: bool = True,
|
||||||
include_summary_sheet: bool = True,
|
include_summary_sheet: bool = True,
|
||||||
|
sort_by: str = "sample_time",
|
||||||
|
sort_order: str = "asc",
|
||||||
) -> Path:
|
) -> Path:
|
||||||
output_dir = (job_dir / "output").resolve()
|
output_dir = (job_dir / "output").resolve()
|
||||||
target = (output_dir / relpath).resolve()
|
target = (output_dir / relpath).resolve()
|
||||||
@@ -241,6 +258,8 @@ def find_output_file(
|
|||||||
include_basic_sheets=include_basic_sheets,
|
include_basic_sheets=include_basic_sheets,
|
||||||
include_unmatched_items=include_unmatched_items,
|
include_unmatched_items=include_unmatched_items,
|
||||||
include_summary_sheet=include_summary_sheet,
|
include_summary_sheet=include_summary_sheet,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
)
|
)
|
||||||
return export_target
|
return export_target
|
||||||
|
|
||||||
@@ -258,6 +277,8 @@ def _apply_export_options(
|
|||||||
include_basic_sheets: bool,
|
include_basic_sheets: bool,
|
||||||
include_unmatched_items: bool,
|
include_unmatched_items: bool,
|
||||||
include_summary_sheet: bool,
|
include_summary_sheet: bool,
|
||||||
|
sort_by: str,
|
||||||
|
sort_order: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not include_basic_sheets and not include_summary_sheet:
|
if not include_basic_sheets and not include_summary_sheet:
|
||||||
raise ProcessingError("\u81f3\u5c11\u9700\u8981\u5bfc\u51fa\u57fa\u672c\u5de5\u4f5c\u8868\u6216\u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868\u3002")
|
raise ProcessingError("\u81f3\u5c11\u9700\u8981\u5bfc\u51fa\u57fa\u672c\u5de5\u4f5c\u8868\u6216\u672a\u68c0\u6d4b\u5230\u5185\u5bb9\u6c47\u603b\u8868\u3002")
|
||||||
@@ -278,6 +299,7 @@ def _apply_export_options(
|
|||||||
if not workbook.worksheets:
|
if not workbook.worksheets:
|
||||||
workbook.create_sheet(SUMMARY_SHEET_NAME)
|
workbook.create_sheet(SUMMARY_SHEET_NAME)
|
||||||
|
|
||||||
|
_sort_workbook(workbook, sort_by, sort_order)
|
||||||
workbook.save(path)
|
workbook.save(path)
|
||||||
finally:
|
finally:
|
||||||
workbook.close()
|
workbook.close()
|
||||||
@@ -434,6 +456,8 @@ def _postprocess_workbook(
|
|||||||
include_basic_sheets: bool,
|
include_basic_sheets: bool,
|
||||||
include_unmatched_items: bool,
|
include_unmatched_items: bool,
|
||||||
include_summary_sheet: bool,
|
include_summary_sheet: bool,
|
||||||
|
sort_by: str,
|
||||||
|
sort_order: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
workbook = load_workbook(path)
|
workbook = load_workbook(path)
|
||||||
try:
|
try:
|
||||||
@@ -455,6 +479,7 @@ def _postprocess_workbook(
|
|||||||
if not workbook.worksheets:
|
if not workbook.worksheets:
|
||||||
workbook.create_sheet(SUMMARY_SHEET_NAME)
|
workbook.create_sheet(SUMMARY_SHEET_NAME)
|
||||||
|
|
||||||
|
_sort_workbook(workbook, sort_by, sort_order)
|
||||||
workbook.save(path)
|
workbook.save(path)
|
||||||
finally:
|
finally:
|
||||||
workbook.close()
|
workbook.close()
|
||||||
@@ -631,6 +656,134 @@ def _remove_unmatched_columns(workbook) -> None:
|
|||||||
sheet.delete_cols(unmatched_col + 1, sheet.max_column - unmatched_col)
|
sheet.delete_cols(unmatched_col + 1, sheet.max_column - unmatched_col)
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_workbook(workbook, sort_by: str, sort_order: str) -> None:
|
||||||
|
sort_by = sort_by if sort_by in SORT_FIELDS else "sample_time"
|
||||||
|
sort_order = sort_order if sort_order in SORT_ORDERS else "asc"
|
||||||
|
if sort_by == "none":
|
||||||
|
return
|
||||||
|
|
||||||
|
reverse = sort_order == "desc"
|
||||||
|
for sheet in workbook.worksheets:
|
||||||
|
if sheet.max_row < 3:
|
||||||
|
continue
|
||||||
|
if sheet.title == SUMMARY_SHEET_NAME:
|
||||||
|
_sort_summary_sheet(sheet, sort_by, reverse)
|
||||||
|
else:
|
||||||
|
_sort_regular_sheet(sheet, sort_by, reverse)
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_regular_sheet(sheet, sort_by: str, reverse: bool) -> None:
|
||||||
|
header = [_cell_text(sheet.cell(1, col).value) for col in range(1, sheet.max_column + 1)]
|
||||||
|
sort_col = _sort_column_from_header(header, sort_by)
|
||||||
|
if sort_col is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
rows = _read_rows(sheet, 2, sheet.max_row)
|
||||||
|
_sort_rows(rows, sort_col, sort_by, reverse)
|
||||||
|
_write_rows(sheet, 2, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_summary_sheet(sheet, sort_by: str, reverse: bool) -> None:
|
||||||
|
row_index = 1
|
||||||
|
while row_index <= sheet.max_row:
|
||||||
|
row = [_cell_text(sheet.cell(row_index, col).value) for col in range(1, min(sheet.max_column, 4) + 1)]
|
||||||
|
if not _is_summary_header_values(row):
|
||||||
|
row_index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
header = [_cell_text(sheet.cell(row_index, col).value) for col in range(1, sheet.max_column + 1)]
|
||||||
|
sort_col = _sort_column_from_header(header, sort_by)
|
||||||
|
if sort_col is None:
|
||||||
|
row_index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = row_index + 1
|
||||||
|
end = start
|
||||||
|
while end <= sheet.max_row:
|
||||||
|
next_row = [
|
||||||
|
_cell_text(sheet.cell(end, col).value)
|
||||||
|
for col in range(1, min(sheet.max_column, 4) + 1)
|
||||||
|
]
|
||||||
|
if _is_summary_header_values(next_row):
|
||||||
|
break
|
||||||
|
end += 1
|
||||||
|
|
||||||
|
if end > start:
|
||||||
|
rows = _read_rows(sheet, start, end - 1)
|
||||||
|
_sort_rows(rows, sort_col, sort_by, reverse)
|
||||||
|
_write_rows(sheet, start, rows)
|
||||||
|
row_index = end
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_column_from_header(header: list[str], sort_by: str) -> int | None:
|
||||||
|
if sort_by == "name":
|
||||||
|
names = ["姓名"]
|
||||||
|
elif sort_by == "sample_time":
|
||||||
|
names = ["采样时间"]
|
||||||
|
elif sort_by == "reason":
|
||||||
|
names = ["检测原因"]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for index, value in enumerate(header):
|
||||||
|
if any(value == name or value.startswith(name) for name in names):
|
||||||
|
return index
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _read_rows(sheet, start: int, end: int) -> list[list[object]]:
|
||||||
|
return [
|
||||||
|
[sheet.cell(row_index, col).value for col in range(1, sheet.max_column + 1)]
|
||||||
|
for row_index in range(start, end + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _write_rows(sheet, start: int, rows: list[list[object]]) -> None:
|
||||||
|
for offset, row in enumerate(rows):
|
||||||
|
row_index = start + offset
|
||||||
|
for col, value in enumerate(row, start=1):
|
||||||
|
sheet.cell(row_index, col).value = value
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_rows(rows: list[list[object]], sort_col: int, sort_by: str, reverse: bool) -> None:
|
||||||
|
rows.sort(key=lambda row: _sort_value(row[sort_col], sort_by))
|
||||||
|
if reverse:
|
||||||
|
filled = [row for row in rows if _cell_text(row[sort_col])]
|
||||||
|
empty = [row for row in rows if not _cell_text(row[sort_col])]
|
||||||
|
filled.reverse()
|
||||||
|
rows[:] = filled + empty
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_value(value: object, sort_by: str) -> tuple[int, object]:
|
||||||
|
text = _cell_text(value)
|
||||||
|
if not text:
|
||||||
|
return (1, "")
|
||||||
|
if sort_by == "sample_time":
|
||||||
|
parsed = _parse_datetime(text)
|
||||||
|
if parsed is not None:
|
||||||
|
return (0, parsed.isoformat())
|
||||||
|
return (0, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_datetime(value: str) -> datetime | None:
|
||||||
|
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S", "%Y-%m-%d", "%Y/%m/%d"):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(value, fmt)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_summary_header_values(row: list[str]) -> bool:
|
||||||
|
return (
|
||||||
|
len(row) >= 4
|
||||||
|
and row[0] == "姓名"
|
||||||
|
and row[1] == "住院号"
|
||||||
|
and row[2] == "采样时间"
|
||||||
|
and row[3].startswith("检测原因")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _find_header_index(header: list[str], name: str) -> int | None:
|
def _find_header_index(header: list[str], name: str) -> int | None:
|
||||||
for index, value in enumerate(header):
|
for index, value in enumerate(header):
|
||||||
if value == name:
|
if value == name:
|
||||||
|
|||||||
Reference in New Issue
Block a user