8 Commits

Author SHA1 Message Date
david a6ebbce4cf Update metadata.txt 2026-05-19 15:25:38 +02:00
david 88149fbb30 Hotfix/security (#37)
* vyměnit xml.etree.ElementTree za defusedxml.ElementTree

* zpátky xml.etree.ElementTree, ale přidáno # nosec; jde o false positive

* switch z ukládání hesel v registrech na používání QGIS Auth Manager
2026-05-19 15:22:35 +02:00
david c0d054d22a Update metadata.txt
v2.0.0-alpha.1
experimental flag true
2026-05-19 13:12:32 +02:00
david ba41039468 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
2026-05-19 13:05:07 +02:00
david 499b3b3f0a Fix/komponenta jako zaklad (#35)
* čištění amcr_tools.py od zbytečných prvků souvisejících se standalone tabulkou komponent

* aplikace nové logiky přípravy dat v případě požadavku na komponenty

* nové aliasy + přidání dat komponent do finálních tabulek

* oprava pořadí polí v tabulce
2026-05-16 14:18:17 +02:00
david 54f154b264 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
2026-05-14 14:02:18 +02:00
david c679e776df Přejmenování názvů atributových polí (#31)
* changed attribute field names

* added aliases
2026-04-16 09:39:49 +02:00
david a5604dfaa8 Update README.md (#30)
* update README.md (v1.3.2)
2026-04-13 12:49:13 +02:00
7 changed files with 22610 additions and 13849 deletions
+59 -12
View File
@@ -2,7 +2,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
**Platform:** QGIS 3.4.x
**Platform:** QGIS 3.4.04.99.0
**Module Type:** Data Acquisition & Visualization
@@ -22,8 +22,6 @@
* **Dynamic Geometry Retrieval:** Automatically downloads and categorizes spatial data into Point, Line, and Polygon layers.
* **Semantic Interoperability:** Automatically translates internal system codes into human-readable labels using the AIS CR API.
---
## 2. Installation Guide
**Install the plugin from QGIS plugin repository.**
@@ -37,8 +35,6 @@
*5. Locate the source ZIP file and click Install Plugin.*
*6. Upon successful installation, the AMCR download button (load AMCR data) will appear in the interface.*
---
## 3. User Manual
### 3.1 Data Retrieval
@@ -64,8 +60,6 @@ To initiate a search query, click either the **Stáhnout data akcí** or the **S
For a more in-depth tutorial refer to the [AMČR Documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
### 3.2 Layer Structure & Attributes
Upon successful retrieval, the plugin generates four temporary memory layers:
@@ -77,11 +71,63 @@ Upon successful retrieval, the plugin generates four temporary memory layers:
The Attribute Table includes standardized fields with important metadata. The components layer has no geometry on its own and depend solely on a relation with the other three layers.
---
#### 3.2.1 Fields of the layers with geometry
| Field | Description |
| --- | --- |
| PIAN | PIAN (spatial identifier) ID |
| Přesnost | spatial deviation [in units/tens/hundreds of meters/defined by cadastre] |
| PIAN typ | [point/line/polygon] |
| Dokumentační jednotka | Documentation unit ID |
| Typ dokumentační jednotky | [trench/event part/whole event/cadastral territory] |
| Definiční bod(y) (WGS-84) | feature centroid in WGS-84 coordinate system |
| Akce/Lokalita | Event/Site ID |
| Odkaz do Digitálního archivu AMČR | link to the Event/Site record in the Digital Archive |
| Okres | district |
| Katastr | main cadastre |
| Další katastr | other cadastres, if the event extends beyond the main cadastre |
| Přístupnost | record accessibility [A/B/C/D] |
> Common fields
| Field | Description |
| --- | --- |
| Vedoucí akce | main fieldwork manager |
| Organizace | organization conducting the research |
| Specifikace data | [exact date/exact years/sometime in years] |
| Datum zahájení | Event start date |
| Datum ukončení | Event end date |
| Hlavní typ | primary research method [total excavation/pit trench/surface collection survey/…] |
| Vedlejší typ | secondary research method [same options as in Hlavní typ] |
| Zjištění | did the research reveal archaeological contexts? [positive/negative] |
| Akce lokalizace | verbal description of the event location |
| Akce nahrazuje NZ | replaces a fieldwork report? [yes/no] |
> Fields related to *Fieldwork events*
| Field | Description |
| --- | --- |
| nazev_lokality | site name |
| popis_lokality | site description |
| typ_lokality | site classification by definition method [survey polygon/heritage site/landscape] |
| druh_lokality | site classification by the nature of identified field relics [aerial survey polygon/landscape/remains of settlement/…] |
| zachovalost | site preservation state [buried site/ruin/aboveground remains/…] |
> Fields related to *Sites*
#### 3.2.2 Fields of the *Components* layer
| Field | Description |
| --- | --- |
| komponenta | Component ID |
| dj_id | parent Documentation unit ID |
| komponenta_areal | Activity area [settlement/burial area/field/…] |
| komponenta_obdobi | Period [Neolithic/High Middle AgesModern Period/Middle La Tène (LtBC1)/…] |
| vrstva | system value linking to a specific geometry table with the corresponding documentation unit |
## 4. Technical Architecture
The plugin is developed in **Python 3** using the **PyQt5** framework for the GUI and the **Requests** library for HTTP communication.
The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
### 4.1 File Structure
@@ -100,12 +146,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
* Parameters: `entity=akce`, `rows/page` (pagination).
* Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability.
2. **Translation API:**
* Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json`
* Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session.
### 4.3 Data Persistence
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
@@ -117,7 +161,10 @@ The plugin interacts with three primary endpoints of the AIS CR infrastructure:
* **Record Limit:** A safety cap of 20,000 records is enforced.
* **Batch Processing:** Geometry fetching is batched (50 IDs per request) to comply with URL length limitations and server load balancing.
## 6. Links and resources
### 4.5 Relational Data Linking
The plugin automatically utilizes advanced QGIS features for data relationship management, specifically Polymorphic Relations. The *Components* layer is dynamically linked within the project to the spatial layers of events and sites via the documentation unit identifier (dj_id). This allows users to immediately see all *components* belonging to a given geometry (point, line, or polygon) directly in the attribute form or the identify features tool, without the need to manually filter or join the data.
## 5. Links and resources
* [AMCR/Digiarchive Documentation](https://amcr-help.aiscr.cz/) (only in Czech).
* [AMCR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
+182 -85
View File
@@ -2,10 +2,38 @@
import os
import csv
import requests
import xml.etree.ElementTree as ET # nosec B314
import time
from qgis.core import QgsMessageLog, Qgis
# Define paths for the plugin and its codelists directory
PLUGIN_DIR = os.path.dirname(__file__)
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
BASE_URL = "https://api.aiscr.cz/2.2/oai"
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
slovnicek = {
'obdobi' : 'heslo:obdobi',
'typ_akce' : 'heslo:akce_typ',
'areal' : 'heslo:areal',
'kraj' : 'ruian_kraj',
'organizace' : 'organizace',
'okres' : 'ruian_okres',
'katastr' : 'ruian_katastr',
'vedouci' : 'osoba',
'pian_presnost' : 'heslo:pian_presnost',
'typ_lokality' : 'heslo:lokalita_typ',
'druh_lokality' : 'heslo:lokalita_druh',
'jistota' : 'heslo:jistota_urceni',
'lokalita_zachovalost' : 'heslo:stav_dochovani',
'pristupnost' : 'heslo:pristupnost'
}
NS = {
'oai': 'http://www.openarchives.org/OAI/2.0/',
'dc': 'http://purl.org/dc/elements/1.1/',
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
}
def ensure_codelists_dir():
"""Creates the codelists directory if it does not exist."""
@@ -46,101 +74,170 @@ def parse_codelist_file(filename, target_dict=None):
# Assign the extracted code to the corresponding label within the category
target_dict[cat][label] = clean
except Exception as e:
print(f"AMČR Codelist Read Error for {filename}: {e}")
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
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()
# Initialize the base structure with empty dictionaries for all expected categories
categorized_data = {
'obdobi': {}, 'typ_akce': {}, 'areal': {},
'kraj': {}, 'organizace': {}, 'okres': {}, 'katastr': {},
'vedouci': {}, 'pian_presnost': {}, 'typ_lokality': {}, 'druh_lokality': {},
'jistota': {}, 'lokalita_zachovalost': {}
}
# Parse the default static codelist and the dynamically generated leaders codelist
categorized_data = {k: {} for k in slovnicek.keys()}
parse_codelist_file('heslar.csv', categorized_data)
parse_codelist_file('vedouci.csv', categorized_data)
return categorized_data
def download_vedouci():
"""Fetches the list of leaders from the AMČR API and saves it to a CSV file."""
def fetch_set(internal_name, api_set, task=None):
dataset = []
params = {
"verb": "ListRecords",
"metadataPrefix": "oai_dc",
"set": api_set
}
while True:
# Kontrola zrušení v každém kroku
if task and task.isCanceled():
return None
try:
response = requests.get(BASE_URL, params=params, timeout=30)
response.raise_for_status()
root = ET.fromstring(response.content)
records = root.findall('.//oai:record', NS)
for rec in records:
metadata = rec.find('.//oai_dc:dc', NS)
if metadata is not None:
# Kód (identifier)
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
titles = metadata.findall('dc:title', NS)
nazev = ""
for t in titles:
if t.text and not t.text.startswith("AMČR -") and not t.text.startswith(" AMČR -"):
nazev = t.text
break
# Pokud by náhodou žádný title neprošel filtrem, vezmeme první dostupný
if not nazev and titles:
nazev = titles[0].text
specialni_pripady = ['okres', 'katastr']
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,
'Kategorie': internal_name
})
# Stránkování
token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text:
params = {
"verb": "ListRecords",
"resumptionToken": token.text
}
time.sleep(0.5)
else:
break
except Exception as e:
QgsMessageLog.logMessage(f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.Warning)
break
return dataset
def download_heslare(task=None):
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
ensure_codelists_dir()
all_data = []
total_sets = len(slovnicek)
# API endpoint for fetching facet data for leaders
url = "https://digiarchiv.aiscr.cz/api/search/query?entity=akce&sort=datestamp%20desc&page=0&onlyFacets=True&rows=0"
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
# Pokud uživatel task zrušil v liště QGISu
if task and task.isCanceled():
return False
QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
# Nyní předáváme task správně do upravené funkce
data = fetch_set(interni, api_nazev, task=task)
if data is None:
return False # Bylo zrušeno uprostřed stahování
all_data.extend(data)
# Reportování postupu (0-100)
if task:
progress = (index + 1) / total_sets * 100
task.setProgress(progress)
# Uložení do CSV
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['Název', 'Kód', 'Kategorie']
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
writer.writeheader()
writer.writerows(all_data)
return True
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, PRISTUPNOST
try:
# Execute the GET request with a 20-second timeout
r = requests.get(url, timeout=20)
r.raise_for_status()
data = r.json()
# Extract the leaders list from the JSON response using safe dict getters
vedouci_list = data.get('facet_counts', {}).get('f_vedouci', [])
if not vedouci_list:
vedouci_list = data.get('facet_counts', {}).get('facet_fields', {}).get('f_vedouci', [])
csv_path = os.path.join(CODELISTS_DIR, 'vedouci.csv')
count = 0
# Open the target CSV file for writing without extra blank lines
with open(csv_path, 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f, delimiter=';')
# Write the standard header required by the parser function
writer.writerow(['Název', 'Kód', 'Kategorie'])
# Iterate through the API results and format them for the CSV
for item in vedouci_list:
name = None
if isinstance(item, dict):
name = item.get('name')
elif isinstance(item, str):
name = item
# Ignore pure numbers (which are usually counts) and write valid names
if name and not str(name).isdigit():
writer.writerow([name, name, 'vedouci'])
count += 1
return True, f"Staženo {count} jmen."
except Exception as e:
return False, str(e)
# Initialize global codelist data when the module is imported
_DATA = load_all_data()
# Safely extract individual categories into global variables for easy access across the plugin
OBDOBI = _DATA.get('obdobi', {})
TYP_AKCE = _DATA.get('typ_akce', {})
AREAL = _DATA.get('areal', {})
KRAJE = _DATA.get('kraj', {})
ORGANIZACE = _DATA.get('organizace', {})
OKRESY = _DATA.get('okres', {})
KATASTRY = _DATA.get('katastr', {})
VEDOUCI = _DATA.get('vedouci', {})
PIAN_PRESNOST = _DATA.get('pian_presnost', {})
TYP_LOKALITY = _DATA.get('typ_lokality', {})
DRUH_LOKALITY = _DATA.get('druh_lokality', {})
JISTOTA = _DATA.get('jistota', {})
LOKALITA_ZACHOVALOST = _DATA.get('lokalita_zachovalost', {})
def refresh_vedouci_cache():
"""Reloads only the 'vedouci.csv' file to quickly update the cache without full initialization."""
# Parse only the targeted file containing the updated leaders
temp_data = parse_codelist_file('vedouci.csv')
new_vedouci = temp_data.get('vedouci', {})
data = load_all_data()
# Clear the existing global dictionary and update it with the fresh data
OBDOBI.clear()
OBDOBI.update(data.get('obdobi', {}))
TYP_AKCE.clear()
TYP_AKCE.update(data.get('typ_akce', {}))
AREAL.clear()
AREAL.update(data.get('areal', {}))
KRAJE.clear()
KRAJE.update(data.get('kraj', {}))
ORGANIZACE.clear()
ORGANIZACE.update(data.get('organizace', {}))
OKRESY.clear()
OKRESY.update(data.get('okres', {}))
KATASTRY.clear()
KATASTRY.update(data.get('katastr', {}))
VEDOUCI.clear()
VEDOUCI.update(new_vedouci)
return len(VEDOUCI)
VEDOUCI.update(data.get('vedouci', {}))
PIAN_PRESNOST.clear()
PIAN_PRESNOST.update(data.get('pian_presnost', {}))
TYP_LOKALITY.clear()
TYP_LOKALITY.update(data.get('typ_lokality', {}))
DRUH_LOKALITY.clear()
DRUH_LOKALITY.update(data.get('druh_lokality', {}))
JISTOTA.clear()
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 = {}
TYP_AKCE = {}
AREAL = {}
KRAJE = {}
ORGANIZACE = {}
OKRESY = {}
KATASTRY = {}
VEDOUCI = {}
PIAN_PRESNOST = {}
TYP_LOKALITY = {}
DRUH_LOKALITY = {}
JISTOTA = {}
LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {}
refresh_globals()
+308 -35
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)
from qgis.PyQt.QtCore import Qt
QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
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)
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):
"""
@@ -99,13 +128,12 @@ 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': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
}
@@ -137,20 +165,17 @@ 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":
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 +213,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 +252,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 +279,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"
@@ -284,6 +324,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():
@@ -305,4 +347,235 @@ class AmcrFilterDialog(QDialog):
if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
return filters
return filters
class LoginDialog(QDialog):
"""
Dialog for saving AMČR login credentials securely in the QGIS Authentication Manager.
Credentials are encrypted by the platform's native secret storage
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
The auth config ID is persisted in QSettings so the session can be
restored automatically after a QGIS restart.
Note on QgsAuthManager quirks (QGIS 4 / Python bindings):
- hasConfigId() is unreliable it checks an in-memory cache that may not
be populated yet. We never use it as a hard gate; we skip it and call
loadAuthenticationConfig() directly instead.
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
SIP_INOUT on their config parameter, so Python bindings return a tuple
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
"""
SETTINGS_KEY = "amcr_viewer/auth_config_id"
CONFIG_NAME = "AMČR Viewer"
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Přihlášení do AMČR")
self.setMinimumWidth(360)
layout = QVBoxLayout()
# Check whether a config ID is already stored from a previous session.
# We attempt a lightweight load (full=False) to confirm it is readable,
# since hasConfigId() may return False even for valid configs (cache lag).
existing_id = QSettings().value(self.SETTINGS_KEY, "")
self._has_saved = bool(existing_id) and bool(self._load_username_from_config(existing_id))
if self._has_saved:
info = QLabel("✔ Přihlašovací údaje jsou bezpečně uloženy ve správci autentizace QGIS.\n"
"Vyplňte pole níže pouze pokud je chcete změnit.")
info.setStyleSheet("color: green; font-style: italic;")
else:
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
info.setWordWrap(True)
layout.addWidget(info)
layout.addSpacing(8)
form = QFormLayout()
self.txt_user = QLineEdit()
self.txt_user.setPlaceholderText("např. jan.novak@email.cz")
# Pre-fill the stored username (not sensitive) for convenience
if self._has_saved:
self.txt_user.setText(self._load_username_from_config(existing_id))
form.addRow("E-mail:", self.txt_user)
self.txt_pass = QLineEdit()
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
self.txt_pass.setPlaceholderText(
"ponechte prázdné pro zachování stávajícího hesla" if self._has_saved else "heslo"
)
form.addRow("Heslo:", self.txt_pass)
layout.addLayout(form)
layout.addSpacing(8)
if self._has_saved:
btn_forget = QPushButton("Odebrat 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)
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
@staticmethod
def _load_config(config_id: str, full: bool = False):
"""
Attempt to load a QgsAuthMethodConfig by ID.
Returns (ok, cfg). Never raises; returns (False, empty cfg) on any error.
full=True decrypts and includes the password.
"""
try:
auth_mgr = QgsApplication.authManager()
cfg = QgsAuthMethodConfig()
result = auth_mgr.loadAuthenticationConfig(config_id, cfg, full)
# Python bindings return (bool, cfg) due to SIP_INOUT parameter
if isinstance(result, tuple):
return result
return result, cfg
except Exception:
return False, QgsAuthMethodConfig()
def _load_username_from_config(self, config_id: str) -> str:
"""Load just the username from a stored config (no password decryption)."""
ok, cfg = self._load_config(config_id, full=False)
return cfg.config("username", "") if ok else ""
def _ensure_master_password(self) -> bool:
"""
Ensure the Auth Manager is unlocked before writing.
Prompts the user to set or enter the master password if needed.
Returns True if the manager is ready, False if the user cancelled.
"""
auth_mgr = QgsApplication.authManager()
if auth_mgr.isDisabled():
QMessageBox.critical(
self, "Správce autentizace nedostupný",
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
"Zkuste obnovit databázi: Nastavení → Možnosti → Autentizace → Pomůcky."
)
return False
# setMasterPassword(True) shows the QGIS master password dialog if needed
if not auth_mgr.setMasterPassword(True):
return False # User cancelled the master password dialog
return True
# ------------------------------------------------------------------
# Button actions
# ------------------------------------------------------------------
def _save_and_accept(self):
username = self.txt_user.text().strip()
password = self.txt_pass.text()
if not username:
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím e-mailovou adresu.")
return
existing_id = QSettings().value(self.SETTINGS_KEY, "")
auth_mgr = QgsApplication.authManager()
# If a config already exists and the password field is blank,
# update only the username and keep the existing encrypted password.
if not password and existing_id:
ok, cfg = self._load_config(existing_id, full=True)
if ok:
if not self._ensure_master_password():
return
cfg.setConfig("username", username)
auth_mgr.updateAuthenticationConfig(cfg)
self.accept()
return
if not password:
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
return
if not self._ensure_master_password():
return
cfg = QgsAuthMethodConfig()
cfg.setName(self.CONFIG_NAME)
cfg.setMethod("Basic")
cfg.setConfig("username", username)
cfg.setConfig("password", password) # nosec B106
settings = QSettings()
# Try to update an existing config first; fall back to creating a new one.
# We skip hasConfigId() as it may return False despite the config existing
# (in-memory cache may not be populated yet in QGIS 4).
ok_load, existing_cfg = self._load_config(existing_id, full=False) if existing_id else (False, None)
if ok_load:
cfg.setId(existing_id)
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
else:
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
config_id = cfg.id() if cfg else ""
if not ok or not config_id:
QMessageBox.critical(
self, "Chyba uložení",
"Přihlašovací údaje se nepodařilo uložit do správce autentizace QGIS.\n"
"Zkuste restartovat QGIS a přihlásit se znovu."
)
return
settings.setValue(self.SETTINGS_KEY, config_id)
self.accept()
def _forget_credentials(self):
settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id:
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
settings.remove(self.SETTINGS_KEY)
QMessageBox.information(self, "Hotovo", "Uložené přihlašovací údaje byly odebrány.")
self.reject()
# ------------------------------------------------------------------
# Public static API call this anywhere in the plugin to get credentials
# ------------------------------------------------------------------
@staticmethod
def get_credentials() -> tuple[str, str]:
"""
Retrieve (username, password) from the QGIS Authentication Manager.
Returns ('', '') if no credentials are stored or the manager is locked.
Note: hasConfigId() is intentionally skipped here it checks an
in-memory cache that may lag behind the actual database contents,
causing false negatives (see class docstring). loadAuthenticationConfig()
is called directly and its return value is used as the authoritative result.
"""
settings = QSettings()
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
if not config_id:
return "", ""
ok, cfg = LoginDialog._load_config(config_id, full=True)
if not ok:
return "", ""
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
+196 -102
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from qgis.core import (QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
QgsField, QgsCoordinateReferenceSystem, QgsCoordinateTransform,
QgsWkbTypes, QgsPolymorphicRelation, QgsEditorWidgetSetup, 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
@@ -86,7 +196,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
current_page = 0
BATCH_DOCS = 500 # Records per API request
MAX_LIMIT = 20000 # Safety limit to prevent QGIS from freezing
feats_k = [] # List for component features (non-spatial)
seen_ids = set()
target_pian_ids_count = 0
@@ -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', [])
@@ -146,7 +256,6 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# pian_lookup maps a Geometry ID (PIAN) to a list of its associated metadata
pian_lookup = {}
komponenty_lookup = {}
target_pian_ids = set()
actions_with_geom = 0
@@ -247,23 +356,35 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
dj_pian_value = dj_pian.get('id')
if dj_pian_value:
target_pian_ids.add(dj_pian_value)
target_pian_ids_count += 1
if dj_pian_value not in pian_lookup:
pian_lookup[dj_pian_value] = []
pian_lookup[dj_pian_value].append(dj_meta)
# Parse non-spatial components if requested (for relational tables)
if komponenty == "true":
# One feature per component — all data on a single row, no relations needed
komps = dj.get('dj_komponenta', [])
for komp in komps:
komp_temp = [
komp.get('ident_cely', ""),
komp.get('komponenta_areal', {}).get('value', ""),
komp.get('komponenta_obdobi', {}).get('value', "")
]
if dj_id not in komponenty_lookup:
komponenty_lookup[dj_id] = []
komponenty_lookup[dj_id].append(komp_temp)
if komps:
for komp in komps:
komp_meta = {
**dj_meta,
'komponenta_id': komp.get('ident_cely', ""),
'komponenta_areal': komp.get('komponenta_areal', {}).get('value', ""),
'komponenta_obdobi': komp.get('komponenta_obdobi', {}).get('value', ""),
}
pian_lookup[dj_pian_value].append(komp_meta)
target_pian_ids_count += 1
else:
# DJ without components — still include with empty component fields
empty_meta = {
**dj_meta,
'komponenta_id': "",
'komponenta_areal': "",
'komponenta_obdobi': "",
}
pian_lookup[dj_pian_value].append(empty_meta)
target_pian_ids_count += 1
else:
target_pian_ids_count += 1
pian_lookup[dj_pian_value].append(dj_meta)
if not target_pian_ids:
@@ -297,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:
@@ -317,32 +438,32 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# Define attribute table structure
cols = [
QgsField("PIAN", QMetaType.Type.QString),
QgsField("Přesnost", QMetaType.Type.QString),
QgsField("PIAN typ", QMetaType.Type.QString),
QgsField("Dokumentační jednotka", QMetaType.Type.QString),
QgsField("Typ dokumentační jednotky", QMetaType.Type.QString),
QgsField("Definiční bod(y) (WGS-84)", QMetaType.Type.QString),
QgsField(archeologicky_zaznam, QMetaType.Type.QString),
QgsField("Odkaz do Digitálního archivu AMČR", QMetaType.Type.QString),
QgsField("Okres", QMetaType.Type.QString),
QgsField("Katastr", QMetaType.Type.QString),
QgsField("Další katastry", QMetaType.Type.QString)
QgsField("pian", QMetaType.Type.QString),
QgsField("presnost", QMetaType.Type.QString),
QgsField("pian_typ", QMetaType.Type.QString),
QgsField("dj", QMetaType.Type.QString),
QgsField("typ_dj", QMetaType.Type.QString),
QgsField("definicni_body", QMetaType.Type.QString),
QgsField(typ_dat, QMetaType.Type.QString),
QgsField("odkaz_do_digiarchivu", QMetaType.Type.QString),
QgsField("okres", QMetaType.Type.QString),
QgsField("katastr", QMetaType.Type.QString),
QgsField("dalsi_katastry", QMetaType.Type.QString)
]
# Extend table based on data type
if typ_dat == "akce":
cols += [
QgsField("Akce lokalizace", QMetaType.Type.QString),
QgsField("Vedoucí akce", QMetaType.Type.QString),
QgsField("Organizace", QMetaType.Type.QString),
QgsField("Specifikace data", QMetaType.Type.QString),
QgsField("Datum zahájeni", QMetaType.Type.QString),
QgsField("Datum ukončení", QMetaType.Type.QString),
QgsField("Hlavní typ", QMetaType.Type.QString),
QgsField("Vedlejší typ", QMetaType.Type.QString),
QgsField("Zjištění", QMetaType.Type.QString),
QgsField("Akce nahrazuje NZ", QMetaType.Type.QString),
QgsField("akce_lokalizace", QMetaType.Type.QString),
QgsField("vedouci", QMetaType.Type.QString),
QgsField("organizace", QMetaType.Type.QString),
QgsField("specifikace_data", QMetaType.Type.QString),
QgsField("zahajeni", QMetaType.Type.QString),
QgsField("ukonceni", QMetaType.Type.QString),
QgsField("hlavni_typ", QMetaType.Type.QString),
QgsField("vedlejsi_typ", QMetaType.Type.QString),
QgsField("zjisteni", QMetaType.Type.QString),
QgsField("nahrazuje_NZ", QMetaType.Type.QString),
]
elif typ_dat == "lokalita":
cols += [
@@ -357,29 +478,43 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
# Use aliases for technical field names
alias_map = {
"pian": "PIAN",
"presnost": "Přesnost",
"pian_typ": "PIAN typ",
"dj": "Dokumentační jednotka",
"typ_dj": "Typ dokumentační jednotky",
"definicni_body": "Definiční bod(y) (WGS-84)",
typ_dat: archeologicky_zaznam,
"odkaz_do_digiarchivu": "Odkaz do Digitálního archivu AMČR",
"okres": "Okres",
"katastr": "Katastr",
"dalsi_katastry": "Další katastry",
"akce_lokalizace": "Akce lokalizace",
"vedouci": "Vedoucí akce",
"organizace": "Organizace",
"specifikace_data": "Specifikace data",
"zahajeni": "Datum zahájeni",
"ukonceni": "Datum ukončení",
"hlavni_typ": "Hlavní typ",
"vedlejsi_typ": "Vedlejší typ",
"zjisteni": "Zjištění",
"nahrazuje_NZ": "Akce nahrazuje NZ",
"nazev_lokality": "Název lokality",
"popis_lokality": "Popis lokality",
"typ_lokality": "Typ lokality",
"druh_lokality": "Druh lokality",
"zachovalost": "Zachovalost"
"zachovalost": "Zachovalost",
"komponenta": "Komponenta",
"komponenta_areal": "Areál",
"komponenta_obdobi": "Období",
}
# Create a non-spatial table for components if requested
if komponenty == "true":
vl_komponenty = QgsVectorLayer("None", "AMCR Komponenty", "memory")
pr = vl_komponenty.dataProvider()
komponenty_cols = [
cols += [
QgsField("komponenta", QMetaType.Type.QString),
QgsField("dj_id", QMetaType.Type.QString),
QgsField("komponenta_areal", QMetaType.Type.QString),
QgsField("komponenta_obdobi", QMetaType.Type.QString),
QgsField("vrstva", QMetaType.Type.QString)
]
pr.addAttributes(komponenty_cols)
vl_komponenty.updateFields()
idx_vrstva = vl_komponenty.fields().indexOf("vrstva")
vl_komponenty.setEditorWidgetSetup(idx_vrstva, QgsEditorWidgetSetup("Hidden", {}))
for vl in layers:
vl.dataProvider().addAttributes(cols)
@@ -464,13 +599,16 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
meta['lokalita_typ'], meta['lokalita_druh'],
meta['lokalita_zachovalost']
])
if komponenty == "true" and meta['dj_id'] in komponenty_lookup:
for k in komponenty_lookup[meta['dj_id']]:
if len(k) == 3:
k.append(referenced_layer.id())
atributy.append(meta['pristupnost'])
if komponenty == "true":
atributy.extend([
meta.get('komponenta_id', ""),
meta.get('komponenta_areal', ""),
meta.get('komponenta_obdobi', ""),
])
feat.setAttributes(atributy)
target_list.append(feat)
@@ -478,18 +616,7 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
print(f"Chyba při tvorbě feature: {ex}")
pass
if komponenty == "true":
for k in komponenty_lookup:
for komp in komponenty_lookup[k]:
if len(komp) == 4:
feat = QgsFeature()
atributy = [
komp[0], k, komp[1], komp[2], komp[3]
]
feat.setAttributes(atributy)
feats_k.append(feat)
# --- ADDING TO QGIS INTERFACE ---
# --- ADDING TO QGIS INTERFACE ---
proj = QgsProject.instance()
added = 0
layers_to_process = [
@@ -498,49 +625,16 @@ def load_amcr_data(canvas, bb, filters=None, typ_dat="akce", komponenty="false")
(feats_pt, vl_point, "Body"),
]
if komponenty == "true":
layers_to_process.append((feats_k, vl_komponenty, "Komponenty"))
for f, l, n in layers_to_process:
if f:
l.dataProvider().addFeatures(f)
l.updateExtents()
l.setName(f"AMCR_{archeologicky_zaznam}_{n}")
proj.addMapLayer(l)
if n != "Komponenty":
added += len(f)
added += len(f)
if added > 0:
iface.messageBar().pushMessage("AMCR", f"Hotovo. Záznamů: {len(docs)} (s geom: {actions_with_geom}). Vykresleno: {added} prvků.", level=Qgis.MessageLevel.Success)
# --- RELATIONSHIP MANAGEMENT ---
# Set up automatic links between spatial layers and the component table
if komponenty == "true":
parent_layers_ids = []
if feats_p:
parent_layers_ids.append(vl_poly.id())
if feats_l:
parent_layers_ids.append(vl_line.id())
if feats_pt:
parent_layers_ids.append(vl_point.id())
rel_manager = proj.relationManager()
rel = QgsPolymorphicRelation()
# rel.setId(f"rel_komponenty_{archeologicky_zaznam}")
rel.setName("Komponenty")
rel.setReferencingLayer(vl_komponenty.id())
rel.setReferencedLayerExpression("@layer_id")
rel.setReferencedLayerField("vrstva")
rel.setReferencedLayerIds(parent_layers_ids)
rel.addFieldPair("dj_id", "Dokumentační jednotka")
rel.generateId()
if rel.isValid():
rel_manager.addPolymorphicRelation(rel)
else:
print("Relace Komponenty není validní!")
else:
iface.messageBar().pushMessage("AMCR", "Žádná data k zobrazení.", level=Qgis.MessageLevel.Info)
+30 -3
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())
@@ -168,4 +180,19 @@ 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)
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
)
+21832 -13606
View File
File diff suppressed because it is too large Load Diff
+3 -6
View File
@@ -8,11 +8,11 @@ name=AMČR Viewer
qgisMinimumVersion=3.4.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data.
version=1.3.2
version=2.0.0-alpha.2
author=David Spáčil
email=spacil@arub.cz
about=This plugin is intended for downloading the data (Fieldwork events, Sites and their Components) from the Digiarchive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/). As of now, only publicly accessible data can be downloaded.
about=This plugin is intended for downloading the data (Fieldwork events, Sites, and their Components) from the Digital archive of the Archaeological Map of the Czech Republic (https://digiarchiv.aiscr.cz/).
tracker=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/issues
repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
@@ -31,7 +31,7 @@ homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
category=Vector
icon=download.png
# experimental flag
experimental=False
experimental=True
# deprecated flag (applies to the whole plugin, not just a single version)
deprecated=False
@@ -41,9 +41,6 @@ deprecated=False
# Check the documentation for more information.
# plugin_dependencies=
# Category of the plugin: Raster, Vector, Database or Web
# category=
# If the plugin can run on QGIS Server.
server=False