add: schau regelmäßig nach neuerem Excel file auf Netzlaufwerk oder zeige Warnung wenn nicht erreichbar

This commit is contained in:
Rob committed 2026-05-21 23:25:47 +02:00
1 parent 7d82aeb0f5
commit 2eef2c1052
1 file changed
+197 -21
+197 -21
View File
@@ -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())