# -*- coding: utf-8 -*- import base64 from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout, QLineEdit, QDialogButtonBox, QCheckBox, QGroupBox, QPushButton, QListWidget, QListWidgetItem, QHBoxLayout, QMessageBox, QLabel, QFormLayout) from qgis.PyQt.QtCore import Qt, QSettings 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, PRISTUPNOST, 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): """ A custom dialog for selecting multiple items from a list with a search filter. Updated for PyQt6/Qt6 compatibility. """ def __init__(self, title, data_dict, preselected_codes, parent=None): super().__init__(parent) self.setWindowTitle(f"Výběr: {title}") self.resize(400, 500) # Store the source data and previously selected items self.data_dict = data_dict self.preselected = preselected_codes if preselected_codes else [] layout = QVBoxLayout() # Setup search input for filtering items self.search_bar = QLineEdit() self.search_bar.setPlaceholderText("Hledat v seznamu...") self.search_bar.textChanged.connect(self.filter_list) layout.addWidget(self.search_bar) # Main list widget for displaying selectable items self.list_widget = QListWidget() self.populate_list() layout.addWidget(self.list_widget) # Standard OK/Cancel dialog buttons buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) self.setLayout(layout) def populate_list(self): # Sort items alphabetically by their display name sorted_names = sorted(self.data_dict.keys()) for name in sorted_names: code = self.data_dict[name] item = QListWidgetItem(name) # Store the actual code (ID) hidden in the UserRole item.setData(Qt.ItemDataRole.UserRole, code) # Make the item checkable (adds a checkbox) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) # Restore previous selection state if code in self.preselected: item.setCheckState(Qt.CheckState.Checked) else: item.setCheckState(Qt.CheckState.Unchecked) self.list_widget.addItem(item) def filter_list(self, text): # Hide items that don't match the search text (case-insensitive) search_text = text.lower() for i in range(self.list_widget.count()): item = self.list_widget.item(i) item.setHidden(search_text not in item.text().lower()) def get_selected_codes(self): """Returns the hidden codes and display labels of all checked items.""" codes = [] labels = [] for i in range(self.list_widget.count()): item = self.list_widget.item(i) if item.checkState() == Qt.CheckState.Checked: codes.append(item.data(Qt.ItemDataRole.UserRole)) labels.append(item.text()) return codes, labels # --- Main window --- class AmcrFilterDialog(QDialog): """ The main filtering UI where users set criteria before downloading data. """ def __init__(self, typ_dat, parent=None): super().__init__(parent) self.setWindowTitle("Filtr AMČR") self.resize(500, 750) # 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': [], 'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': [] } layout = QVBoxLayout() # Filter by current map canvas extent self.chk_bbox = QCheckBox("Omezit vyhledávání rozsahem okna") self.chk_bbox.setChecked(True) layout.addWidget(self.chk_bbox) # Positive/negative evidence – valid for Akce if self.typ_dat == "akce": self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění") layout.addWidget(self.chk_posevidence) layout.addSpacing(10) # Spatial information – valid for all self.picker_kraj = self.setup_picker("Kraj", 'kraj', KRAJE) layout.addWidget(self.picker_kraj) self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY) layout.addWidget(self.picker_okres) self.picker_katastr = self.setup_picker("Katastr", 'katastr', KATASTRY) layout.addWidget(self.picker_katastr) self.picker_presnost = self.setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST) layout.addWidget(self.picker_presnost) self.picker_pristupnost = self.setup_picker("Přístupnost", 'pristupnost', PRISTUPNOST) layout.addWidget(self.picker_pristupnost) # Filters valid for Akce if self.typ_dat == "akce": self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE) layout.addWidget(self.picker_org) self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI) layout.addWidget(self.picker_vedouci) # Type of event self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE) layout.addWidget(self.picker_typ) # Filters valid for Lokality if self.typ_dat == "lokalita": self.picker_typ_lokality = self.setup_picker("Lokalita – typ", 'typ_lokality', TYP_LOKALITY) layout.addWidget(self.picker_typ_lokality) self.picker_druh_lokality = self.setup_picker("Lokalita – druh", 'druh_lokality', DRUH_LOKALITY) layout.addWidget(self.picker_druh_lokality) self.picker_jistota = self.setup_picker("Lokalita – jistota určení", 'jistota', JISTOTA) layout.addWidget(self.picker_jistota) self.picker_lokalita_zachovalost = self.setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST) layout.addWidget(self.picker_lokalita_zachovalost) # Contextual information self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI) layout.addWidget(self.picker_obdobi) self.picker_areal = self.setup_picker("Areál", 'areal', AREAL) layout.addWidget(self.picker_areal) # Option to download related components table self.chk_komponenty = QCheckBox("Načíst komponenty") layout.addWidget(self.chk_komponenty) # Pushes everything above to the top layout.addStretch(1) # 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) self.setLayout(layout) def setup_picker(self, label_text, cache_key, data_source, extra_btn=None): """ Creates a reusable UI component consisting of a label, a read-only text field showing selected items, and a button to open the selection dialog. """ row_widget = QGroupBox(label_text) row_layout = QHBoxLayout() row_layout.setContentsMargins(5, 5, 5, 5) # Read-only field displaying the names of selected items display_field = QLineEdit() display_field.setReadOnly(True) display_field.setPlaceholderText("Nic nevybráno (vše)") display_field.setStyleSheet("background-color: #f0f0f0; color: #333;") btn = QPushButton("Vybrat...") btn.setFixedWidth(80) # 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: codes, labels = dlg.get_selected_codes() # Update local cache with selected IDs self.selection_cache[cache_key] = codes # Update the UI text field with selected names if labels: display_field.setText(", ".join(labels)) else: display_field.clear() # Special case: Pre-fill specific accuracy levels by default if cache_key == 'pian_presnost': display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů") self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863'] btn.clicked.connect(open_dialog) row_layout.addWidget(display_field) row_layout.addWidget(btn) # Add an optional extra button (e.g., the refresh button for leaders) if extra_btn: row_layout.addWidget(extra_btn) row_widget.setLayout(row_layout) return row_widget 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: 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" def get_komponenty(self): return "true" if self.chk_komponenty.isChecked() else "false" def get_filters(self): """Compiles the user selections from the cache into API-ready filter parameters.""" filters = {} if self.selection_cache['kraj']: filters['f_kraj'] = self.selection_cache['kraj'] if self.selection_cache['okres']: filters['f_okres'] = self.selection_cache['okres'] if self.selection_cache['katastr']: filters['f_katastr'] = self.selection_cache['katastr'] if self.selection_cache['obdobi']: filters['f_obdobi'] = self.selection_cache['obdobi'] if self.selection_cache['areal']: filters['f_areal'] = self.selection_cache['areal'] if self.selection_cache['pian_presnost']: filters['f_pian_presnost'] = self.selection_cache['pian_presnost'] if self.selection_cache['pristupnost']: filters['pristupnost'] = self.selection_cache['pristupnost'] if self.typ_dat == "akce": if self.chk_posevidence.isChecked(): filters['posevidence'] = 'true' if self.selection_cache['organizace']: filters['f_organizace'] = self.selection_cache['organizace'] if self.selection_cache['typ_akce']: filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce'] if self.selection_cache['vedouci']: filters['f_vedouci'] = self.selection_cache['vedouci'] if self.typ_dat == "lokalita": if self.selection_cache['typ_lokality']: filters['f_typ_lokality'] = self.selection_cache['typ_lokality'] if self.selection_cache['druh_lokality']: filters['f_druh_lokality'] = self.selection_cache['druh_lokality'] if self.selection_cache['jistota']: filters['f_jistota'] = self.selection_cache['jistota'] if self.selection_cache['lokalita_zachovalost']: filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost'] return filters class LoginDialog(QDialog): """ Dialog pro uložení přihlašovacích údajů do AMČR. Ukládá do QSettings (username plaintext, heslo base64). """ KEY_USER = "amcr_viewer/username" KEY_PASS = "amcr_viewer/password" def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Přihlášení do AMČR") self.setMinimumWidth(360) layout = QVBoxLayout() settings = QSettings() has_saved = bool(settings.value(self.KEY_USER, "")) if has_saved: info = QLabel("✔ Přihlašovací údaje jsou uloženy. Vyplňte pole níže pro jejich změnu.") info.setStyleSheet("color: green; font-style: italic;") else: info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\nVarování: přihlašovací údaje budou nezašifrovaně uloženy v registrech systému.") info.setWordWrap(True) layout.addWidget(info) layout.addSpacing(8) form = QFormLayout() self.txt_user = QLineEdit() self.txt_user.setPlaceholderText("např. jan.novak@email.cz") # Předvyplnit uložené jméno pro pohodlí self.txt_user.setText(settings.value(self.KEY_USER, "")) form.addRow("E-mail:", self.txt_user) self.txt_pass = QLineEdit() self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password) self.txt_pass.setPlaceholderText("heslo") form.addRow("Heslo:", self.txt_pass) layout.addLayout(form) layout.addSpacing(8) if has_saved: btn_forget = QPushButton("Zapomenout uložené přihlašovací údaje") btn_forget.setStyleSheet("color: #c0392b;") btn_forget.clicked.connect(self._forget_credentials) layout.addWidget(btn_forget) layout.addStretch(1) buttons = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttons.accepted.connect(self._save_and_accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) self.setLayout(layout) def _save_and_accept(self): username = self.txt_user.text().strip() password = self.txt_pass.text() if not username or not password: QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím uživatelské jméno i heslo.") return settings = QSettings() settings.setValue(self.KEY_USER, username) # base64 není šifrování, ale heslo aspoň neleží v plaintextu v registru settings.setValue(self.KEY_PASS, base64.b64encode(password.encode()).decode()) self.accept() def _forget_credentials(self): settings = QSettings() settings.remove(self.KEY_USER) settings.remove(self.KEY_PASS) QMessageBox.information(self, "Hotovo", "Přihlašovací údaje byly odstraněny.") self.reject() @staticmethod def get_credentials() -> tuple[str, str]: """Vrátí (username, password) z QSettings, nebo ('', '') pokud nejsou uloženy.""" settings = QSettings() username = settings.value(LoginDialog.KEY_USER, "") encoded = settings.value(LoginDialog.KEY_PASS, "") try: password = base64.b64decode(encoded.encode()).decode() if encoded else "" except Exception: password = "" return username, password