Feature/aktualizace heslaru (#32)

* přechod od statického hesláře k dynamickému načítání z OAI-PMH API AMČR

* aplikace načítání heslářů a task management (backend)

* frontend + debugging

* aktualizace přibaleného hesláře

* kosmetické drobnosti

* ošetření speciální případů při stahování hesláře (katastr, okres) + s tím spojená aktualizace přiloženého hesláře
This commit is contained in:
2026-05-14 14:02:18 +02:00
committed by GitHub
parent c679e776df
commit 54f154b264
3 changed files with 22066 additions and 13720 deletions
+68 -31
View File
@@ -1,14 +1,43 @@
# -*- coding: utf-8 -*-
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QFormLayout,
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QLabel, QMessageBox, QApplication, QWidget)
QMessageBox)
from qgis.PyQt.QtCore import Qt
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST,
download_vedouci, refresh_vedouci_cache)
download_heslare, refresh_globals)
class UpdateCodelistsTask(QgsTask):
def __init__(self, description):
super().__init__(description, QgsTask.CanCancel)
self.success = False
self.exception = None
def run(self):
"""Tato část běží ve vedlejším vlákně."""
try:
# Voláme upravenou funkci
self.success = download_heslare(task=self)
return self.success
except Exception as e:
self.exception = e
return False
def finished(self, result):
"""Tato část běží v hlavním vlákně po skončení run()."""
if result:
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
refresh_globals()
QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info)
else:
if self.isCanceled():
QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning)
else:
QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical)
class FilterableSelectionDialog(QDialog):
"""
@@ -99,9 +128,8 @@ class AmcrFilterDialog(QDialog):
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
self.typ_dat = typ_dat
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
@@ -143,14 +171,8 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce":
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
layout.addWidget(self.picker_org)
# Button to fetch fresh project leaders from the API
self.btn_update_vedouci = QPushButton("🔄")
self.btn_update_vedouci.setToolTip("Aktualizovat seznam vedoucích z API")
self.btn_update_vedouci.setFixedWidth(30)
self.btn_update_vedouci.clicked.connect(self.action_update_vedouci)
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI, extra_btn=self.btn_update_vedouci)
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI)
layout.addWidget(self.picker_vedouci)
# Type of event
@@ -188,10 +210,18 @@ class AmcrFilterDialog(QDialog):
# Pushes everything above to the top
layout.addStretch(1)
# Main dialog OK/Cancel buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
# Main dialog OK/Cancel/Update buttons
buttons = QDialogButtonBox()
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
self.btn_update.setToolTip("Provede kompletní aktualizaci heslářů AMČR. Toto bude trvat pár minut.")
self.btn_update.clicked.connect(self.action_update_heslare)
buttons.addButton(self.btn_update, QDialogButtonBox.ButtonRole.ActionRole)
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
@@ -219,7 +249,7 @@ class AmcrFilterDialog(QDialog):
# Nested function that handles opening the dialog and saving results
def open_dialog():
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
if dlg.exec() == QDialog.DialogCode.Accepted: # PyQt6: DialogCode
if dlg.exec() == QDialog.DialogCode.Accepted:
codes, labels = dlg.get_selected_codes()
# Update local cache with selected IDs
self.selection_cache[cache_key] = codes
@@ -246,21 +276,28 @@ class AmcrFilterDialog(QDialog):
row_widget.setLayout(row_layout)
return row_widget
def action_update_vedouci(self):
# Change cursor to loading state to indicate background task
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
try:
success, msg = download_vedouci()
if success:
count = refresh_vedouci_cache()
QMessageBox.information(self, "Úspěch", f"{msg}\nNyní je v paměti {count} osob.")
def action_update_heslare(self):
# Vytvoření instance tasku
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
# Povolíme tlačítko zpět bez ohledu na výsledek
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány."))
# Ošetření, aby se přesně ukázala případná chyba
def on_error():
if task.exception:
# Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
else:
QMessageBox.warning(self, "Chyba", f"Nepodařilo se stáhnout data:\n{msg}")
except Exception as e:
QMessageBox.critical(self, "Chyba", str(e))
finally:
# Safely restore the normal cursor even if an error occurs
QApplication.restoreOverrideCursor()
msg = "Aktualizace byla zrušena uživatelem."
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
task.taskTerminated.connect(on_error)
QgsApplication.taskManager().addTask(task)
def get_bbox(self):
return "true" if self.chk_bbox.isChecked() else "false"