add: schau regelmäßig nach neuerem Excel file auf Netzlaufwerk oder zeige Warnung wenn nicht erreichbar
This commit is contained in:
1 parent
7d82aeb0f5
commit
2eef2c1052
1 file changed
+197
-21
@@ -107,6 +107,11 @@ def _count_output_pdfs(config: Dict[str, Any]) -> int:
|
|||||||
|
|
||||||
_UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json"
|
_UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json"
|
||||||
_INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE)
|
_INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE)
|
||||||
|
_SOURCE_EXCEL_DIR = Path(
|
||||||
|
r"j:\Fachbereiche\BO\Produktionsschule\Maßnahmeunterlagen\30_Teilnehmende\01_Anwesenheit\Anwesenheitsliste"
|
||||||
|
)
|
||||||
|
_SOURCE_YEAR_PATTERN = re.compile(r"(20\d{2})")
|
||||||
|
_SOURCE_CHECK_INTERVAL_MS = 60_000
|
||||||
|
|
||||||
|
|
||||||
def _parse_semver(version_str: str) -> Tuple[int, int, int]:
|
def _parse_semver(version_str: str) -> Tuple[int, int, int]:
|
||||||
@@ -173,6 +178,90 @@ def _find_latest_installer(config: Dict[str, Any]) -> Optional[Tuple[str, Path]]
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_year_from_excel_name(filename: str) -> Optional[int]:
|
||||||
|
"""Extract a four-digit year from an Excel filename."""
|
||||||
|
match = _SOURCE_YEAR_PATTERN.search(filename)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(match.group(1))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_source_excel(files: List[Path]) -> Optional[Path]:
|
||||||
|
"""Pick the most relevant source Excel (current year preferred)."""
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_year = date.today().year
|
||||||
|
with_year: List[Tuple[Path, int]] = []
|
||||||
|
without_year: List[Path] = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
year = _extract_year_from_excel_name(file.name)
|
||||||
|
if year is None:
|
||||||
|
without_year.append(file)
|
||||||
|
else:
|
||||||
|
with_year.append((file, year))
|
||||||
|
|
||||||
|
current_year_files = [f for f, y in with_year if y == current_year]
|
||||||
|
if current_year_files:
|
||||||
|
return max(current_year_files, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
if with_year:
|
||||||
|
newest_year = max(y for _, y in with_year)
|
||||||
|
newest_year_files = [f for f, y in with_year if y == newest_year]
|
||||||
|
return max(newest_year_files, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
return max(without_year, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_source_excel_update() -> Dict[str, Any]:
|
||||||
|
"""Check source folder connectivity and whether a newer Excel is available."""
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"connected": False,
|
||||||
|
"update_available": False,
|
||||||
|
"source_file": None,
|
||||||
|
"target_file": None,
|
||||||
|
"status": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not _SOURCE_EXCEL_DIR.exists():
|
||||||
|
result["status"] = "Netzlaufwerk nicht erreichbar."
|
||||||
|
return result
|
||||||
|
|
||||||
|
source_files = sorted(_SOURCE_EXCEL_DIR.glob("*.xlsx"), key=lambda p: p.name.lower())
|
||||||
|
result["connected"] = True
|
||||||
|
|
||||||
|
if not source_files:
|
||||||
|
result["status"] = "Keine Excel-Datei in der Quelle gefunden."
|
||||||
|
return result
|
||||||
|
|
||||||
|
source_file = _choose_source_excel(source_files)
|
||||||
|
if source_file is None:
|
||||||
|
result["status"] = "Keine geeignete Excel-Datei in der Quelle gefunden."
|
||||||
|
return result
|
||||||
|
|
||||||
|
target_file = _input_dir() / source_file.name
|
||||||
|
result["source_file"] = source_file
|
||||||
|
result["target_file"] = target_file
|
||||||
|
|
||||||
|
if (not target_file.exists()) or (source_file.stat().st_mtime > target_file.stat().st_mtime):
|
||||||
|
result["update_available"] = True
|
||||||
|
result["status"] = f"Neue Excel verfügbar: {source_file.name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["status"] = f"Excel aktuell: {source_file.name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
result["connected"] = False
|
||||||
|
result["status"] = "Netzlaufwerk nicht erreichbar."
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
||||||
"""Return deduplicated month keys from config (preserves insertion order)."""
|
"""Return deduplicated month keys from config (preserves insertion order)."""
|
||||||
seen: set = set()
|
seen: set = set()
|
||||||
@@ -248,6 +337,7 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._out_dir: Optional[Path] = None
|
self._out_dir: Optional[Path] = None
|
||||||
self._input_files: List[Path] = []
|
self._input_files: List[Path] = []
|
||||||
self._excel_name_to_path: Dict[str, Path] = {}
|
self._excel_name_to_path: Dict[str, Path] = {}
|
||||||
|
self._pending_source_excel: Optional[Path] = None
|
||||||
self._tooltip_window: Optional[ctk.CTkToplevel] = None
|
self._tooltip_window: Optional[ctk.CTkToplevel] = None
|
||||||
self._tooltip_label: Optional[tk.Label] = None
|
self._tooltip_label: Optional[tk.Label] = None
|
||||||
|
|
||||||
@@ -258,8 +348,8 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._build_preview_section()
|
self._build_preview_section()
|
||||||
self._build_log_section()
|
self._build_log_section()
|
||||||
|
|
||||||
# Auto-sync after event loop starts
|
# Initial scan + continuous source check
|
||||||
self.after(100, self._start_initial_sync)
|
self.after(100, self._start_source_excel_watch)
|
||||||
# Daily update check
|
# Daily update check
|
||||||
if _should_check_update():
|
if _should_check_update():
|
||||||
self.after(200, self._start_update_check)
|
self.after(200, self._start_update_check)
|
||||||
@@ -348,6 +438,21 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
)
|
)
|
||||||
self._excel_menu.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="w")
|
self._excel_menu.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="w")
|
||||||
|
|
||||||
|
self._btn_excel_update = ctk.CTkButton(
|
||||||
|
frame,
|
||||||
|
text="Update",
|
||||||
|
width=90,
|
||||||
|
height=28,
|
||||||
|
command=self._on_excel_update_click,
|
||||||
|
)
|
||||||
|
self._btn_excel_update.grid(row=0, column=3, padx=(0, 6), pady=6, sticky="w")
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
|
||||||
|
self._lbl_source_warning = ctk.CTkLabel(
|
||||||
|
frame, text="", width=24, anchor="w", text_color="orange"
|
||||||
|
)
|
||||||
|
self._lbl_source_warning.grid(row=0, column=4, padx=(0, 12), pady=6, sticky="w")
|
||||||
|
|
||||||
# Row 1: Template
|
# Row 1: Template
|
||||||
ctk.CTkLabel(frame, text="Template:", width=110, anchor="w").grid(
|
ctk.CTkLabel(frame, text="Template:", width=110, anchor="w").grid(
|
||||||
row=1, column=0, padx=(12, 4), pady=6, sticky="w"
|
row=1, column=0, padx=(12, 4), pady=6, sticky="w"
|
||||||
@@ -557,6 +662,32 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._excel_menu.configure(values=[text], state="disabled")
|
self._excel_menu.configure(values=[text], state="disabled")
|
||||||
self._excel_var.set(text)
|
self._excel_var.set(text)
|
||||||
|
|
||||||
|
def _set_source_state(
|
||||||
|
self,
|
||||||
|
connected: bool,
|
||||||
|
update_available: bool,
|
||||||
|
source_file: Optional[Path],
|
||||||
|
status: str,
|
||||||
|
) -> None:
|
||||||
|
self._pending_source_excel = source_file if update_available else None
|
||||||
|
|
||||||
|
if not connected:
|
||||||
|
self._lbl_source_warning.configure(text="⚠")
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
self._set_download_status(status or "Netzlaufwerk nicht erreichbar.", "orange")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._lbl_source_warning.configure(text="")
|
||||||
|
if update_available and source_file is not None:
|
||||||
|
self._btn_excel_update.grid()
|
||||||
|
self._btn_excel_update.configure(state="normal", text="Update")
|
||||||
|
self._set_download_status(status, "#60A5FA")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
if status:
|
||||||
|
self._set_download_status(status, "#22C55E")
|
||||||
|
|
||||||
def _set_download_status(self, text: str, color: str = "#60A5FA") -> None:
|
def _set_download_status(self, text: str, color: str = "#60A5FA") -> None:
|
||||||
self._lbl_download_status.configure(text=text, text_color=color)
|
self._lbl_download_status.configure(text=text, text_color=color)
|
||||||
|
|
||||||
@@ -703,33 +834,78 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._excel_var.set(selected_name)
|
self._excel_var.set(selected_name)
|
||||||
self._excel_path = self._excel_name_to_path[selected_name]
|
self._excel_path = self._excel_name_to_path[selected_name]
|
||||||
|
|
||||||
def _start_initial_sync(self) -> None:
|
def _start_source_excel_watch(self) -> None:
|
||||||
self._set_excel_loading_state("Lade Excel vom Netzlaufwerk ...")
|
self._scan_and_validate()
|
||||||
self._set_download_status("Datei vom Netzlaufwerk wird geprueft ...")
|
self._start_source_excel_check()
|
||||||
threading.Thread(target=self._run_initial_sync, daemon=True).start()
|
|
||||||
|
|
||||||
def _run_initial_sync(self) -> None:
|
def _start_source_excel_check(self) -> None:
|
||||||
|
threading.Thread(target=self._run_source_excel_check, daemon=True).start()
|
||||||
|
|
||||||
|
def _run_source_excel_check(self) -> None:
|
||||||
|
result = _check_source_excel_update()
|
||||||
|
self.after(0, self._on_source_excel_check_done, result)
|
||||||
|
|
||||||
|
def _on_source_excel_check_done(self, result: Dict[str, Any]) -> None:
|
||||||
|
self._set_source_state(
|
||||||
|
connected=bool(result.get("connected")),
|
||||||
|
update_available=bool(result.get("update_available")),
|
||||||
|
source_file=result.get("source_file"),
|
||||||
|
status=str(result.get("status") or ""),
|
||||||
|
)
|
||||||
|
self.after(_SOURCE_CHECK_INTERVAL_MS, self._start_source_excel_check)
|
||||||
|
|
||||||
|
def _on_excel_update_click(self) -> None:
|
||||||
|
source_file = self._pending_source_excel
|
||||||
|
if source_file is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_file = _input_dir() / source_file.name
|
||||||
|
if target_file.exists():
|
||||||
|
proceed = messagebox.askokcancel(
|
||||||
|
"Datei überschreiben",
|
||||||
|
f"Die vorhandene Datei\n{target_file.name}\n"
|
||||||
|
"wird durch die neuere Datei ersetzt.\n\nFortfahren?",
|
||||||
|
icon="warning",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
if not proceed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._btn_excel_update.configure(state="disabled", text="Lade...")
|
||||||
|
self._set_download_status(f"Lade {source_file.name} ...", "#60A5FA")
|
||||||
|
threading.Thread(
|
||||||
|
target=self._run_source_excel_download,
|
||||||
|
args=(source_file,),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def _run_source_excel_download(self, source_file: Path) -> None:
|
||||||
try:
|
try:
|
||||||
synced_path, status = _sync_network_excel(_CONFIG)
|
target_file = _copy_excel_to_input(source_file)
|
||||||
self.after(0, self._finish_initial_sync, synced_path, status, None)
|
self.after(0, self._on_source_excel_download_done, target_file, None)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.after(0, self._finish_initial_sync, None, "", str(exc))
|
self.after(0, self._on_source_excel_download_done, None, str(exc))
|
||||||
|
|
||||||
def _finish_initial_sync(
|
def _on_source_excel_download_done(
|
||||||
self,
|
self,
|
||||||
synced_path: Optional[Path],
|
target_file: Optional[Path],
|
||||||
status: str,
|
|
||||||
error: Optional[str],
|
error: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._btn_excel_update.configure(state="normal", text="Update")
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
self._set_download_status(
|
self._set_download_status(f"Update fehlgeschlagen: {error}", "red")
|
||||||
f"Netzlaufwerk-Pruefung fehlgeschlagen: {error}",
|
return
|
||||||
"orange",
|
|
||||||
)
|
if target_file is None:
|
||||||
else:
|
self._set_download_status("Update fehlgeschlagen.", "red")
|
||||||
color = "#22C55E" if synced_path else "orange"
|
return
|
||||||
self._set_download_status(status, color)
|
|
||||||
self._scan_and_validate(selected_excel=synced_path)
|
self._set_download_status(f"Excel aktualisiert: {target_file.name}", "#22C55E")
|
||||||
|
self._refresh_excel_dropdown(selected=target_file)
|
||||||
|
self._on_excel_changed()
|
||||||
|
self._pending_source_excel = None
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
|
||||||
def _on_excel_changed(self) -> None:
|
def _on_excel_changed(self) -> None:
|
||||||
selected = self._excel_name_to_path.get(self._excel_var.get())
|
selected = self._excel_name_to_path.get(self._excel_var.get())
|
||||||
|
|||||||
Reference in new issue
Block a user