Feature/login (#36)

* přidáno nové tlačítko do toolbaru

* přihlašovací dialog

* funkce pro přihlašování

* aplikace získávání dat z API přes session

* zastřešující akce v main skriptu

* ošetření prodlužování session/opětovné přihlášení

* přidání přístupnosti do hesláře

* aktualizace přibaleného hesláře

* oprava stahování hesláře (pristupnost vyžaduje písmeno [A/B/C/D], ne heslo; aktualizace hesláře

* aplikace přístupnosti do filtračního dialogu

* úpravy přihlašovacího dialogu
This commit is contained in:
2026-05-19 13:05:07 +02:00
committed by GitHub
parent 499b3b3f0a
commit ba41039468
5 changed files with 268 additions and 17 deletions
+11 -3
View File
@@ -25,7 +25,8 @@ slovnicek = {
'typ_lokality' : 'heslo:lokalita_typ',
'druh_lokality' : 'heslo:lokalita_druh',
'jistota' : 'heslo:jistota_urceni',
'lokalita_zachovalost' : 'heslo:stav_dochovani'
'lokalita_zachovalost' : 'heslo:stav_dochovani',
'pristupnost' : 'heslo:pristupnost'
}
NS = {
@@ -78,7 +79,7 @@ def parse_codelist_file(filename, target_dict=None):
return target_dict
def load_all_data():
"""Loads all static and dynamic codelists during plugin startup."""
"""Loads the codelist during plugin startup."""
ensure_codelists_dir()
categorized_data = {k: {} for k in slovnicek.keys()}
parse_codelist_file('heslar.csv', categorized_data)
@@ -125,6 +126,9 @@ def fetch_set(internal_name, api_set, task=None):
if internal_name in specialni_pripady:
kod = nazev
if internal_name == 'pristupnost':
kod = next((t.text for t in titles if t.text and len(t.text) == 1 and t.text.isalpha()), None)
dataset.append({
'Název': nazev,
'Kód': kod,
@@ -186,7 +190,7 @@ def download_heslare(task=None):
def refresh_globals():
"""Znovu načte data ze souborů do globálních proměnných."""
global OBDOBI, TYP_AKCE, AREAL, KRAJE, ORGANIZACE, OKRESY, KATASTRY
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST
data = load_all_data()
@@ -216,6 +220,9 @@ def refresh_globals():
JISTOTA.update(data.get('jistota', {}))
LOKALITA_ZACHOVALOST.clear()
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
PRISTUPNOST.clear()
PRISTUPNOST.update(data.get('pristupnost', {}))
# Inicializace prázdných diktů, které se naplní hned pod tím
OBDOBI = {}
@@ -231,5 +238,6 @@ TYP_LOKALITY = {}
DRUH_LOKALITY = {}
JISTOTA = {}
LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {}
refresh_globals()
+104 -4
View File
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
import base64
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox)
from qgis.PyQt.QtCore import Qt
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,
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
download_heslare, refresh_globals)
class UpdateCodelistsTask(QgsTask):
@@ -133,7 +134,7 @@ class AmcrFilterDialog(QDialog):
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
}
@@ -166,6 +167,9 @@ class AmcrFilterDialog(QDialog):
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":
@@ -321,6 +325,8 @@ class AmcrFilterDialog(QDialog):
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():
@@ -343,3 +349,97 @@ class AmcrFilterDialog(QDialog):
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
+113 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
QgsWkbTypes, Qgis)
QgsWkbTypes, Qgis, QgsApplication, QgsAuthMethodConfig, QgsMessageLog)
from qgis.utils import iface
from qgis.PyQt.QtCore import Qt, QMetaType
from qgis.PyQt.QtWidgets import QApplication
@@ -12,6 +12,116 @@ import json
# Global cache to store translated terms from the Digital Archive
TRANSLATIONS = {}
# Session s autentizační cookie po přihlášení; None = nepřihlášen (anonymní přístup)
AMCR_SESSION: requests.Session | None = None
def _log(msg: str, level=Qgis.MessageLevel.Info):
"""Shortcut: zapíše zprávu do QGIS logu (panel Zprávy → záložka AMČR)."""
QgsMessageLog.logMessage(msg, "AMČR login", level)
def login_to_api(username: str, password: str):
"""
Přihlásí se do Digiarchiv API pomocí username a hesla.
Vrátí requests.Session s nastavenou session cookie, nebo None při chybě.
"""
login_url = "https://digiarchiv.aiscr.cz/api/user/login"
_log(f"Přihlašuji uživatele: '{username}'")
if not username or not password:
_log("CHYBA: username nebo heslo je prázdné.", Qgis.MessageLevel.Critical)
return None
session = requests.Session()
session.headers.update({
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"User-Agent": "QGIS-Plugin/1.0 (AISCR Data Fetcher)"
})
try:
_log(f"Odesílám POST na {login_url} ...")
response = session.post(login_url, json={"user": username, "pwd": password}, timeout=10)
_log(f"HTTP status: {response.status_code}")
response.raise_for_status()
# API vrací chyby se status kódem 200 je nutné zkontrolovat tělo odpovědi
body = response.json()
if "error" in body:
_log(f"CHYBA přihlášení (API): {body['error']}", Qgis.MessageLevel.Critical)
return None
_log("Přihlášení proběhlo úspěšně.")
global AMCR_SESSION
AMCR_SESSION = session
return session
except requests.exceptions.HTTPError as e:
_log(f"CHYBA HTTP {e.response.status_code if e.response else '?'}: "
f"{e.response.text[:300] if e.response else 'žádná odpověď'}", Qgis.MessageLevel.Critical)
return None
except requests.exceptions.RequestException as e:
_log(f"CHYBA sítě: {e}", Qgis.MessageLevel.Critical)
return None
def _get_session() -> requests.Session | None:
"""
Vrátí aktivní session. Pokud žádná není (restart QGIS), pokusí se
automaticky přihlásit pomocí uložených přihlašovacích údajů.
Vrátí None pokud přihlašovací údaje nejsou uloženy.
"""
global AMCR_SESSION
if AMCR_SESSION is not None:
return AMCR_SESSION
# Zkusit auto-login pomocí uložených údajů
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
_log("Session vypršela nebo chybí automatické přihlášení...")
AMCR_SESSION = login_to_api(username, password)
return AMCR_SESSION
def _api_get(url, params, timeout=30) -> requests.Response:
"""
Provede GET request. Pokud API signalizuje vypršení přihlášení,
provede jedno opakované přihlášení a zkusí znovu.
"""
global AMCR_SESSION
def _is_auth_error(resp: requests.Response) -> bool:
"""API vrací auth chyby se status 200 je nutné zkontrolovat tělo."""
if resp.status_code == 401:
return True
try:
body = resp.json()
err = str(body.get("error", "")).lower()
return "unauthorized" in err or "not logged" in err or "session" in err
except Exception:
return False
session = _get_session()
resp = (session or requests).get(url, params=params, timeout=timeout)
if _is_auth_error(resp):
_log("Session vypršela během stahování obnovuji přihlášení...", Qgis.MessageLevel.Warning)
AMCR_SESSION = None # Zrušit starou session
from .amcr_dialog import LoginDialog
username, password = LoginDialog.get_credentials()
if username and password:
AMCR_SESSION = login_to_api(username, password)
if AMCR_SESSION:
resp = AMCR_SESSION.get(url, params=params, timeout=timeout)
else:
_log("Opakované přihlášení selhalo.", Qgis.MessageLevel.Critical)
else:
_log("Přihlašovací údaje nejsou uloženy pokračuji anonymně.", Qgis.MessageLevel.Warning)
return resp
def load_translations():
"""Fetches the official Czech translation dictionary from the AISCR API."""
global TRANSLATIONS
@@ -103,7 +213,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
del base_params['page']
try:
resp_docs = requests.get(url, params=base_params, timeout=30)
resp_docs = _api_get(url, params=base_params, timeout=30)
resp_json = resp_docs.json()
data = resp_json.get('response', {})
batch_docs = data.get('docs', [])
@@ -308,7 +418,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
}
try:
QApplication.processEvents()
r_pian = requests.get(url, params=params_pian, timeout=15)
r_pian = _api_get(url, params=params_pian, timeout=15)
batch_docs = r_pian.json().get('response', {}).get('docs', [])
docs_pian.extend(batch_docs)
except Exception as e:
+29 -2
View File
@@ -2,9 +2,11 @@
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from qgis.core import Qgis
from qgis.utils import iface
from .amcr_tools import load_amcr_data
from .amcr_dialog import AmcrFilterDialog
from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .resources import *
import os.path
@@ -109,6 +111,16 @@ class AmcrViewer:
)
self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action(
icon_path=icon_akce_path,
text=self.tr(u'Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(),
add_to_menu=False,
add_to_toolbar=False
)
self.plugin_menu.addAction(self.action_login_dialog)
# 3. Create the main project action and attach the menu to it
main_icon = QIcon(icon_akce_path)
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
@@ -169,3 +181,18 @@ class AmcrViewer:
# Access the map canvas and start the fetch/render process from amcr_tools
canvas = self.iface.mapCanvas()
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
def login(self):
dlg = LoginDialog(parent=self.iface.mainWindow())
result = dlg.exec()
if result == QDialog.DialogCode.Accepted:
username, password = LoginDialog.get_credentials()
session = login_to_api(username, password)
if session:
self.iface.messageBar().pushMessage(
"AMČR", "Přihlášení proběhlo úspěšně.", level=Qgis.MessageLevel.Success
)
else:
self.iface.messageBar().pushMessage(
"AMČR", "Přihlášení se nezdařilo viz záložka AMČR login v panelu Zprávy.", level=Qgis.MessageLevel.Critical
)
+9 -3
View File
@@ -523,6 +523,9 @@ Děčín;Děčín;okres
Česká Lípa;Česká Lípa;okres
Beroun;Beroun;okres
Benešov;Benešov;okres
Kohoutovice;Kohoutovice;katastr
Nový Lískovec;Nový Lískovec;katastr
Bohunice;Bohunice;katastr
Starý Lískovec;Starý Lískovec;katastr
Horní Heršpice;Horní Heršpice;katastr
Královo Pole;Královo Pole;katastr
@@ -9952,7 +9955,6 @@ Jestřabice;Jestřabice;katastr
Jeníkovec;Jeníkovec;katastr
Týnec nad Labem;Týnec nad Labem;katastr
Horní Čepí;Horní Čepí;katastr
Kohoutovice;Kohoutovice;katastr
Pikárec;Pikárec;katastr
Horní Slověnice;Horní Slověnice;katastr
Porešín;Porešín;katastr
@@ -10586,7 +10588,6 @@ Němčice nad Labem;Němčice nad Labem;katastr
Kozojídky;Kozojídky;katastr
Šemanovice;Šemanovice;katastr
Smolov;Smolov;katastr
Bohunice;Bohunice;katastr
Podolí u Mohelnice;Podolí u Mohelnice;katastr
Běšiny;Běšiny;katastr
Bělice;Bělice;katastr
@@ -11885,7 +11886,6 @@ Hřivínův Újezd;Hřivínův Újezd;katastr
Radonice u Hradiště;Radonice u Hradiště;katastr
Chabičovice;Chabičovice;katastr
Košín;Košín;katastr
Nový Lískovec;Nový Lískovec;katastr
Pavlovice;Pavlovice;katastr
Kněžičky;Kněžičky;katastr
Občov;Občov;katastr
@@ -13614,6 +13614,8 @@ Kameničná;Kameničná;katastr
Hrušová;Hrušová;katastr
Javornice u Dubu;Javornice u Dubu;katastr
Veleslavín;Veleslavín;katastr
Tolar, Jan;OS-008490;vedouci
Šimurda, Petr;OS-008489;vedouci
Mrázek, Michal;OS-008488;vedouci
Hendrych, Marek;OS-008487;vedouci
Haydamakha, Ruslan;OS-008486;vedouci
@@ -21865,3 +21867,7 @@ ruina;HES-001456;lokalita_zachovalost
nadzemní relikty;HES-001455;lokalita_zachovalost
lokalita pod zástavbou;HES-001454;lokalita_zachovalost
zaniklá lokalita;HES-001453;lokalita_zachovalost
archivář;D;pristupnost
archeolog;C;pristupnost
badatel;B;pristupnost
anonym;A;pristupnost
Can't render this file because it is too large.