Compare commits

..
7 Commits
Author SHA1 Message Date
david-spacil 2fdbb15001 feature: aktualizace hesláře 2026-06-29 13:50:53 +02:00
david-spacil 8248d1bcae feature: nové heslářové globals + cache v dialogu 2026-06-29 13:50:39 +02:00
david-spacil abb06b9858 feature: globals pro PAS
založeny nové heslářové globals, pak přidány do funkce refresh_globals
2026-06-29 13:49:12 +02:00
david-spacil e84ee8e543 feature: update funkcí pro stahování dat
funkce fetch_set a download_heslare byly upraveny pro stahování dat nejen z OAI-PMH API, ale nově i z API digiarchivu (= osoby se nově nestahují z hesláře osob, ale z facetek digiarchivu, kde mají osoby přidělené "role" nálezce/vedoucí)
2026-06-29 13:48:23 +02:00
david-spacil 66532c108b feature: rozšíření slovníčku
slovníček interních vs api klíčů byl rozšířen o nová pole (nálezce, okolnosti, ...) a zároveň byla změněna jeho struktura: nově obsahuje i base url vzhledem k tomu, že si skript pro data sahá do dvou různých API; přidáno base url pro digiarchiv
2026-06-29 13:44:54 +02:00
david-spacil 86da8b9cf4 feature: tlačítko v menu
v kontextové nabídce přibylo tlačítko pro filtrování PASových záznamů; ikona je zatím placeholder
2026-06-29 13:41:44 +02:00
david-spacil a95831f180 feature: dialog: přidána nová pole
přidána pole samostatných nálezů pro filtrovací dialog + upravena viditelnost některých spolčných polí (např. skrýt areál, zobrazit organizaci nejen pro akce, ale i pro PAS, ...)
2026-06-29 13:39:44 +02:00
11 changed files with 665 additions and 1318 deletions

No files matched your search

+8 -22
View File
@@ -1,26 +1,15 @@
name: Release QGIS Plugin name: Release QGIS Plugin
# Spouští se na push tagu, ne na publikaci releasu. Organizace má zapnuté
# immutable releases: k už publikovanému releasu nelze nic přiložit, příloha
# tedy musí vzniknout dřív, než se release zveřejní. Workflow proto založí
# koncept releasu i se ZIPem; text se dopisuje a publikuje ručně.
#
# Pozor: workflow se čte z commitu, na který tag ukazuje. Tag musí být
# založen až na commitu, který tento soubor obsahuje.
on: on:
push: release:
tags: types: [published]
- 'v*'
permissions:
contents: write
jobs: jobs:
build-and-release: build-and-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 1. Stáhne kód z tagu, který běh spustil # 1. Stáhne kód z repozitáře
- name: Checkout code - name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -31,14 +20,11 @@ jobs:
# -r = rekurzivně, -x = ignorovat skryté git soubory # -r = rekurzivně, -x = ignorovat skryté git soubory
zip -r amcr_viewer.zip amcr_viewer -x "*.git*" zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# 3. Založí koncept releasu i s přílohou # 3. Nahraje ZIP k Releasu
# Tagy s pomlčkou (v2.0.0-alpha.1) se označí jako pre-release. - name: Upload Release Asset
- name: Create draft release with asset
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
if: startsWith(github.ref, 'refs/tags/')
with: with:
files: amcr_viewer.zip files: amcr_viewer.zip
draft: true
name: AMCR Viewer ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4 -62
View File
@@ -83,58 +83,6 @@ Další pravidla:
definice pole (`QgsField`), naplnění hodnoty z dokumentu, překlad hlavičky definice pole (`QgsField`), naplnění hodnoty z dokumentu, překlad hlavičky
sloupce a export atributů. sloupce a export atributů.
### Kompatibilita s Qt6 / QGIS 4
Plugin cílí na QGIS 3.44 i na QGIS 4 (`qgisMaximumVersion=4.99.0`), tedy na
Qt5 i Qt6 zároveň. **Tohle se drží rigorózně** – ne až před releasem, ale při
každé změně kódu. Chování obou větví se liší tiše: pod Qt5 projde i to, co
QGIS 4 odmítne, takže lokální „funguje mi to“ nic nedokazuje.
Závazná pravidla:
- **Nikdy neimportuj přímo z `PyQt5` ani z `PyQt6`.** Vždy přes shim
`qgis.PyQt.*`. Ten mimo jiné pod Qt6 přetahuje `QAction`, `QActionGroup`
a `QShortcut` z `QtGui`, takže import z `qgis.PyQt.QtWidgets` je správně.
- **Enumy vždy plně kvalifikované (scoped).** `Qgis.MessageLevel.Info`, ne
`Qgis.Info`; `QgsTask.Flag.CanCancel`, ne `QgsTask.CanCancel`;
`QgsWkbTypes.GeometryType.PointGeometry`, ne `QgsWkbTypes.PointGeometry`.
Totéž pro Qt: `Qt.CheckState.Checked`, `QDialogButtonBox.StandardButton.Ok`.
Zkrácené tvary sice v QGIS 4.2 zatím fungují, ale oficiální kontrola je
hlásí a do budoucna mizí.
- **Zdrojové `.py` soubory ukládej bez BOM.** Kontrolní skript čte soubor
jako UTF-8 bez `utf-8-sig` a na BOM spadne s
`SyntaxError: invalid non-printable character U+FEFF`, takže se takový
soubor **vůbec nezkontroluje**. (`codelists/heslar.csv` BOM mít smí, tam je
kvůli Excelu.)
- Nepoužívej API zrušená v Qt6: `exec_()`, `QRegExp`, `QDesktopWidget`,
`QApplication.desktop()`, `Qt.MidButton`, `QFontMetrics.width()`,
`setResizeMode`, atributy `AA_EnableHighDpiScaling` / `AA_UseHighDpiPixmaps`.
- `supportsQt6=True` v `metadata.txt` **nepatří** – bylo zrušeno; o zařazení
mezi „QGIS 4 Ready“ rozhoduje rozsah `qgisMinimumVersion` až
`qgisMaximumVersion`.
Ověření před PR, který mění Python kód:
```sh
# oficiální kontrola, kterou pouští i plugins.qgis.org (pyqgis4-checker)
docker run --rm --pull always --user $(id -u):$(id -g) \
--workdir /workspace/ -v "$(pwd):/workspace/" \
ghcr.io/qgis/pyqgis4-checker:main-ubuntu \
pyqt5_to_pyqt6.py --dry_run --logfile /workspace/pyqt6_checker.log .
```
Prázdný log = čisté. Kontrola je na plugins.qgis.org informativní
(neblokuje schválení), ale nález znamená, že plugin v QGIS 4 dříve nebo
později přestane fungovat.
Když je po ruce QGIS 4 (např. flatpak `org.qgis.qgis`), ověř navíc, že se
plugin pod Qt6 opravdu načte:
```sh
flatpak run --command=sh org.qgis.qgis -c \
'PYTHONPATH=/app/share/qgis/python python3 -c "import qgis.core"'
```
## Verzování a release ## Verzování a release
- Verze pluginu žije v **`amcr_viewer/metadata.txt`** (`version=`). - Verze pluginu žije v **`amcr_viewer/metadata.txt`** (`version=`).
@@ -142,16 +90,10 @@ flatpak run --command=sh org.qgis.qgis -c \
`changelog=` v `metadata.txt` (formát `vX.Y.Z (RRRR-MM-DD)` + odrážky). `changelog=` v `metadata.txt` (formát `vX.Y.Z (RRRR-MM-DD)` + odrážky).
- Datum v changelogu ber z **deterministického zdroje**, ne z paměti, např. - Datum v changelogu ber z **deterministického zdroje**, ne z paměti, např.
`python -c "import datetime; print(datetime.date.today().isoformat())"`. `python -c "import datetime; print(datetime.date.today().isoformat())"`.
- Release se spouští **pushnutím tagu `vX.Y.Z`**, ne publikací releasu - Release se vytváří publikací GitHub Release; workflow
v UI. Workflow `.github/workflows/release_plugin.yml` zabalí složku `.github/workflows/release_plugin.yml` zabalí složku `amcr_viewer/` do
`amcr_viewer/` do `amcr_viewer.zip` a založí **koncept** releasu i s touto `amcr_viewer.zip` a přiloží ji k releasu. Do ZIPu se nesmí dostat `.git*`
přílohou; text se dopíše a release zveřejní ručně. Do ZIPu se nesmí dostat soubory.
`.git*` soubory.
- Organizace má zapnuté **immutable releases** – k publikovanému releasu už
nelze nic přiložit. Proto příloha vzniká na konceptu, ještě před
zveřejněním; workflow spouštěný na `release: published` by vždy selhal.
- Workflow se čte z commitu, na který **tag ukazuje**. Tag proto zakládej až
na commitu, který obsahuje aktuální podobu workflow – jinak se nespustí nic.
## Pull requesty ## Pull requesty
+1
View File
@@ -31,5 +31,6 @@ def classFactory(iface): # pylint: disable=invalid-name
:param iface: A QGIS interface instance. :param iface: A QGIS interface instance.
:type iface: QgsInterface :type iface: QgsInterface
""" """
#
from .amcr_viewer import AmcrViewer from .amcr_viewer import AmcrViewer
return AmcrViewer(iface) return AmcrViewer(iface)
+13 -16
View File
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import csv
import os import os
import time import csv
import xml.etree.ElementTree as ET # nosec
import requests import requests
from qgis.core import Qgis, QgsMessageLog import xml.etree.ElementTree as ET # nosec
import time
from qgis.core import QgsMessageLog, Qgis
# Define paths for the plugin and its codelists directory # Define paths for the plugin and its codelists directory
PLUGIN_DIR = os.path.dirname(__file__) PLUGIN_DIR = os.path.dirname(__file__)
@@ -91,7 +90,7 @@ def parse_codelist_file(filename, target_dict=None):
except Exception as e: except Exception as e:
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"AMČR Codelist Read Error for {filename}: {e}", f"AMČR Codelist Read Error for {filename}: {e}",
"AMČR", Qgis.MessageLevel.Critical) "AMČR", Qgis.Critical)
return target_dict return target_dict
@@ -99,7 +98,7 @@ def parse_codelist_file(filename, target_dict=None):
def load_all_data(): def load_all_data():
"""Loads the codelist during plugin startup.""" """Loads the codelist during plugin startup."""
ensure_codelists_dir() ensure_codelists_dir()
categorized_data = {k: {} for k in slovnicek} categorized_data = {k: {} for k in slovnicek.keys()}
parse_codelist_file('heslar.csv', categorized_data) parse_codelist_file('heslar.csv', categorized_data)
return categorized_data return categorized_data
@@ -125,9 +124,7 @@ def fetch_set(base_url, internal_name, api_set, task=None):
try: try:
if "digiarchiv" not in base_url: if "digiarchiv" not in base_url:
response = requests.get( response = requests.get(base_url, params=params_amcr, timeout=30)
base_url, params=params_amcr, timeout=30
)
response.raise_for_status() response.raise_for_status()
root = ET.fromstring(response.content) # nosec root = ET.fromstring(response.content) # nosec
@@ -196,7 +193,7 @@ def fetch_set(base_url, internal_name, api_set, task=None):
time.sleep(0.5) time.sleep(0.5)
else: else:
break break
else: else:
response = requests.get(base_url, params=params_da, timeout=30) response = requests.get(base_url, params=params_da, timeout=30)
response.raise_for_status() response.raise_for_status()
@@ -213,13 +210,13 @@ def fetch_set(base_url, internal_name, api_set, task=None):
'Kód': nazev, 'Kód': nazev,
'Kategorie': internal_name 'Kategorie': internal_name
}) })
break break
except Exception as e: except Exception as e:
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"Chyba u setu {api_set}: {e}", f"Chyba u setu {api_set}: {e}",
"AMČR", Qgis.MessageLevel.Warning) "AMČR", Qgis.Warning)
break break
return dataset return dataset
@@ -243,7 +240,7 @@ def download_heslare(task=None):
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"Zpracovávám kategorii: {interni}...", f"Zpracovávám kategorii: {interni}...",
"AMČR", Qgis.MessageLevel.Info) "AMČR", Qgis.Info)
# Pass the task correctly to the updated fetch function # Pass the task correctly to the updated fetch function
data = fetch_set(base_url, interni, api_nazev, task=task) data = fetch_set(base_url, interni, api_nazev, task=task)
+41 -247
View File
@@ -1,67 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from qgis.core import ( from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
Qgis, QLineEdit, QDialogButtonBox,
QgsApplication, QCheckBox, QGroupBox, QPushButton,
QgsAuthMethodConfig, QListWidget, QListWidgetItem, QHBoxLayout,
QgsMessageLog, QMessageBox, QLabel, QFormLayout)
QgsTask, from qgis.PyQt.QtCore import Qt, QSettings
) from qgis.core import (QgsTask, QgsApplication,
from qgis.gui import QgsDateEdit QgsMessageLog, Qgis, QgsAuthMethodConfig)
from qgis.PyQt.QtCore import QSettings, Qt
from qgis.PyQt.QtWidgets import (
QCheckBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from qgis.utils import iface from qgis.utils import iface
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
from .amcr_codelists import ( OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
AREAL, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
DRUH_LOKALITY, LOKALITA_ZACHOVALOST, PRISTUPNOST,
DRUH_NALEZU, NALEZ_KATEGORIE, DRUH_NALEZU, SPECIFIKACE,
JISTOTA, NALEZOVE_OKOLNOSTI, NALEZCE,
KATASTRY, download_heslare, refresh_globals)
KRAJE,
LOKALITA_ZACHOVALOST,
NALEZ_KATEGORIE,
NALEZCE,
NALEZOVE_OKOLNOSTI,
OBDOBI,
OKRESY,
ORGANIZACE,
PIAN_PRESNOST,
PRISTUPNOST,
SPECIFIKACE,
TYP_AKCE,
TYP_LOKALITY,
VEDOUCI,
download_heslare,
refresh_globals,
)
# The date filter of the API requires both bounds; a one-sided range makes
# the server fail with an ArrayIndexOutOfBoundsException and '*' is not
# accepted either. An empty picker is therefore replaced by these sentinels,
# which are also the default limits of QgsDateEdit.
DATE_OPEN_FROM = "0001-01-01"
DATE_OPEN_TO = "9999-12-31"
# Shown by a date picker that is left empty
DATE_NULL_TEXT = "neomezeno"
# Keep Python references to running tasks. QgsTaskManager only holds the # Keep Python references to running tasks. QgsTaskManager only holds the
@@ -72,7 +25,7 @@ _ACTIVE_TASKS = []
class UpdateCodelistsTask(QgsTask): class UpdateCodelistsTask(QgsTask):
def __init__(self, description): def __init__(self, description):
super().__init__(description, QgsTask.Flag.CanCancel) super().__init__(description, QgsTask.CanCancel)
self.success = False self.success = False
self.exception = None self.exception = None
@@ -93,18 +46,18 @@ class UpdateCodelistsTask(QgsTask):
refresh_globals() refresh_globals()
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
"Hesláře AMČR byly úspěšně aktualizovány.", "Hesláře AMČR byly úspěšně aktualizovány.",
"AMČR", Qgis.MessageLevel.Info "AMČR", Qgis.Info
) )
else: else:
if self.isCanceled(): if self.isCanceled():
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
"Aktualizace heslářů byla zrušena.", "Aktualizace heslářů byla zrušena.",
"AMČR", Qgis.MessageLevel.Warning "AMČR", Qgis.Warning
) )
else: else:
QgsMessageLog.logMessage( QgsMessageLog.logMessage(
f"Chyba aktualizace: {self.exception}", f"Chyba aktualizace: {self.exception}",
"AMČR", Qgis.MessageLevel.Critical "AMČR", Qgis.Critical
) )
@@ -224,10 +177,6 @@ class AmcrFilterDialog(QDialog):
'nalezce': [], 'nalezce': [],
} }
# Date range pickers, filled by setup_date_range():
# (API field, label for messages, 'from' widget, 'to' widget)
self.date_ranges = []
layout = QVBoxLayout() layout = QVBoxLayout()
# Filter by current map canvas extent # Filter by current map canvas extent
@@ -260,6 +209,13 @@ class AmcrFilterDialog(QDialog):
) )
layout.addWidget(self.picker_katastr) 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( self.picker_pristupnost = self.setup_picker(
"Přístupnost", "Přístupnost",
'pristupnost', 'pristupnost',
@@ -269,15 +225,7 @@ class AmcrFilterDialog(QDialog):
# Filters valid for Akce # Filters valid for Akce
if self.typ_dat in ["lokalita", "akce"]: if self.typ_dat in ["pas", "akce"]:
self.picker_presnost = self.setup_picker(
"PIAN – přesnost",
'pian_presnost',
PIAN_PRESNOST
)
layout.addWidget(self.picker_presnost)
if self.typ_dat in ["samostatny_nalez", "akce"]:
self.picker_org = self.setup_picker( self.picker_org = self.setup_picker(
"Organizace", "Organizace",
'organizace', 'organizace',
@@ -302,12 +250,6 @@ class AmcrFilterDialog(QDialog):
) )
layout.addWidget(self.picker_typ) layout.addWidget(self.picker_typ)
self.box_datum = self.setup_date_range("Datum", [
("akce_datum_zahajeni", "Zahájení", "Datum zahájení"),
("akce_datum_ukonceni", "Ukončení", "Datum ukončení"),
])
layout.addWidget(self.box_datum)
# Filters valid for Lokality # Filters valid for Lokality
if self.typ_dat == "lokalita": if self.typ_dat == "lokalita":
@@ -344,7 +286,7 @@ class AmcrFilterDialog(QDialog):
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI) self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi) layout.addWidget(self.picker_obdobi)
if self.typ_dat == "samostatny_nalez": if self.typ_dat == "pas":
self.picker_nalez_kategorie = self.setup_picker( self.picker_nalez_kategorie = self.setup_picker(
"Kategorie nálezu", "Kategorie nálezu",
'nalez_kategorie', 'nalez_kategorie',
@@ -380,14 +322,7 @@ class AmcrFilterDialog(QDialog):
) )
layout.addWidget(self.picker_nalezce) layout.addWidget(self.picker_nalezce)
# Lokalita has no date field in the index at all, so the block if self.typ_dat != "pas":
# is built only for the two entities that do
self.box_datum = self.setup_date_range("Datum nálezu", [
("samostatny_nalez_datum_nalezu", "", "Datum nálezu"),
])
layout.addWidget(self.box_datum)
if self.typ_dat != "samostatny_nalez":
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL) self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal) layout.addWidget(self.picker_areal)
@@ -409,7 +344,7 @@ class AmcrFilterDialog(QDialog):
self.lbl_komponenty_warning.setVisible(False) self.lbl_komponenty_warning.setVisible(False)
layout.addWidget(self.lbl_komponenty_warning) layout.addWidget(self.lbl_komponenty_warning)
if self.typ_dat != "samostatny_nalez": if self.typ_dat != "pas":
self.chk_komponenty.toggled.connect( self.chk_komponenty.toggled.connect(
self.lbl_komponenty_warning.setVisible self.lbl_komponenty_warning.setVisible
) )
@@ -417,21 +352,6 @@ class AmcrFilterDialog(QDialog):
# Pushes everything above to the top # Pushes everything above to the top
layout.addStretch(1) layout.addStretch(1)
# The filter stack is taller than the window on every entity
# (over 1000 px for 'akce'), so it scrolls. The buttons stay
# outside the scroll area, otherwise the user would have to
# scroll to the bottom just to confirm the dialog.
content = QWidget()
content.setLayout(layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setWidget(content)
outer = QVBoxLayout()
outer.addWidget(scroll)
# Main dialog OK/Cancel/Update buttons # Main dialog OK/Cancel/Update buttons
buttons = QDialogButtonBox() buttons = QDialogButtonBox()
@@ -452,9 +372,9 @@ class AmcrFilterDialog(QDialog):
buttons.accepted.connect(self.accept) buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
outer.addWidget(buttons) layout.addWidget(buttons)
self.setLayout(outer) self.setLayout(layout)
def setup_picker(self, label_text, cache_key, data_source, extra_btn=None): def setup_picker(self, label_text, cache_key, data_source, extra_btn=None):
""" """
@@ -517,94 +437,6 @@ class AmcrFilterDialog(QDialog):
row_widget.setLayout(row_layout) row_widget.setLayout(row_layout)
return row_widget return row_widget
def setup_date_range(self, title, rows):
"""
Creates a compact date range block: one row per API date field,
each with a 'from' and a 'to' picker.
rows is a list of (api_field, row_label, name_for_messages).
An empty row_label is used when the group box title already names
the field, which keeps the single-row variant from repeating itself.
A picker left empty means an open bound; the sentinel is
substituted in get_filters(), not here, so that an untouched
block adds no filter at all.
"""
row_widget = QGroupBox(title)
grid = QGridLayout()
grid.setContentsMargins(5, 5, 5, 5)
grid.setVerticalSpacing(3)
grid.setHorizontalSpacing(6)
for row, (api_field, row_label, name) in enumerate(rows):
if row_label:
grid.addWidget(QLabel(row_label), row, 0)
date_from = self._date_edit(
f"{name} – od (prázdné = bez dolní meze)"
)
date_to = self._date_edit(
f"{name} – do (prázdné = bez horní meze)"
)
separator = QLabel("–")
separator.setAlignment(Qt.AlignmentFlag.AlignCenter)
grid.addWidget(date_from, row, 1)
grid.addWidget(separator, row, 2)
grid.addWidget(date_to, row, 3)
self.date_ranges.append((api_field, name, date_from, date_to))
grid.setColumnStretch(1, 1)
grid.setColumnStretch(3, 1)
row_widget.setLayout(grid)
return row_widget
@staticmethod
def _date_edit(tooltip):
"""
A date picker that may stay empty.
clear() is essential here – setEmpty() looks empty but leaves
isNull() False with today's date, which would silently apply
a filter the user never set.
"""
widget = QgsDateEdit()
widget.setAllowNull(True)
widget.setNullRepresentation(DATE_NULL_TEXT)
widget.setDisplayFormat("d. M. yyyy")
widget.setCalendarPopup(True)
widget.clear()
widget.setToolTip(tooltip)
return widget
def accept(self):
"""
Blocks the dialog on a reversed date range. The API answers such
a query with zero records and no error, which is indistinguishable
from a genuinely empty result.
"""
reversed_ranges = [
name for _, name, date_from, date_to in self.date_ranges
if not date_from.isNull() and not date_to.isNull()
and date_from.date() > date_to.date()
]
if reversed_ranges:
QMessageBox.warning(
self,
"Neplatné rozmezí",
"U těchto filtrů je počáteční datum novější než koncové:\n"
+ "\n".join(f"• {name}" for name in reversed_ranges)
+ "\n\nDotaz by nevrátil žádný záznam. Opravte rozmezí, "
"nebo jedno z polí vyprázdněte."
)
return
super().accept()
def action_update_heslare(self): def action_update_heslare(self):
# Create the task instance and keep a reference so the Python # Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes # wrapper survives until the task finishes
@@ -642,7 +474,7 @@ class AmcrFilterDialog(QDialog):
# This will show exactly what went wrong (e.g. PermissionError) # This will show exactly what went wrong (e.g. PermissionError)
msg = ( msg = (
"Aktualizace selhala z důvodu chyby:\n" "Aktualizace selhala z důvodu chyby:\n"
f"{task.exception!s}" f"{str(task.exception)}"
) )
else: else:
msg = "Aktualizace byla zrušena uživatelem." msg = "Aktualizace byla zrušena uživatelem."
@@ -657,10 +489,7 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_bbox.isChecked() else "false" return "true" if self.chk_bbox.isChecked() else "false"
def get_komponenty(self): def get_komponenty(self):
if self.typ_dat in ["akce", "lokalita"]: return "true" if self.chk_komponenty.isChecked() else "false"
return "true" if self.chk_komponenty.isChecked() else "false"
else:
return "false"
def get_filters(self): def get_filters(self):
"""Compiles the user selections from the cache into """Compiles the user selections from the cache into
@@ -694,7 +523,7 @@ class AmcrFilterDialog(QDialog):
filters['f_vedouci'] = self.selection_cache['vedouci'] filters['f_vedouci'] = self.selection_cache['vedouci']
if self.selection_cache['organizace']: if self.selection_cache['organizace']:
filters['f_organizace'] = self.selection_cache['organizace'] filters['f_organizace'] = self.selection_cache['organizace']
if self.selection_cache['typ_lokality']: if self.selection_cache['typ_lokality']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality'] filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
@@ -703,38 +532,7 @@ class AmcrFilterDialog(QDialog):
if self.selection_cache['jistota']: if self.selection_cache['jistota']:
filters['f_jistota'] = self.selection_cache['jistota'] filters['f_jistota'] = self.selection_cache['jistota']
if self.selection_cache['lokalita_zachovalost']: if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = ( filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
self.selection_cache['lokalita_zachovalost']
)
# Samostatné nálezy
if self.selection_cache['nalez_kategorie']:
filters['f_kategorie'] = self.selection_cache['nalez_kategorie']
if self.selection_cache['druh_nalezu']:
filters['f_druh_nalezu'] = self.selection_cache['druh_nalezu']
if self.selection_cache['specifikace']:
filters['f_specifikace'] = self.selection_cache['specifikace']
if self.selection_cache['nalezove_okolnosti']:
filters['f_nalezove_okolnosti'] = (
self.selection_cache['nalezove_okolnosti']
)
if self.selection_cache['nalezce']:
filters['f_nalezce'] = self.selection_cache['nalezce']
# Date ranges – the API needs both bounds, so an empty picker is
# replaced by a sentinel. A block with both pickers empty adds no
# filter at all; sending the full 0001–9999 range would only
# clutter the log without narrowing anything.
for api_field, _, date_from, date_to in self.date_ranges:
if date_from.isNull() and date_to.isNull():
continue
od = (DATE_OPEN_FROM if date_from.isNull()
else date_from.date().toString("yyyy-MM-dd"))
do = (DATE_OPEN_TO if date_to.isNull()
else date_to.date().toString("yyyy-MM-dd"))
filters[api_field] = f"{od},{do}"
return filters return filters
@@ -756,8 +554,7 @@ class LoginDialog(QDialog):
- storeAuthenticationConfig() and loadAuthenticationConfig() both have - storeAuthenticationConfig() and loadAuthenticationConfig() both have
SIP_INOUT on their config parameter, so Python bindings return a tuple SIP_INOUT on their config parameter, so Python bindings return a tuple
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both. (bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
- loadAuthenticationConfig() with full=False loads only metadata - loadAuthenticationConfig() with full=False loads only metadata (name, method,
(name, method,
id) but NOT the config() values like username/password. Use full=True to id) but NOT the config() values like username/password. Use full=True to
access those. access those.
""" """
@@ -988,7 +785,7 @@ class LoginDialog(QDialog):
# We skip hasConfigId() as it may return False # We skip hasConfigId() as it may return False
# despite the config existing # despite the config existing
# (in-memory cache may not be populated yet in QGIS 4). # (in-memory cache may not be populated yet in QGIS 4).
ok_load, _ = ( ok_load, existing_cfg = (
self._load_config(existing_id, full=False) self._load_config(existing_id, full=False)
if existing_id if existing_id
else (False, None) else (False, None)
@@ -1017,9 +814,7 @@ class LoginDialog(QDialog):
settings = QSettings() settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "") existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id: if existing_id:
QgsApplication.authManager().removeAuthenticationConfig( QgsApplication.authManager().removeAuthenticationConfig(existing_id)
existing_id
)
settings.remove(self.SETTINGS_KEY) settings.remove(self.SETTINGS_KEY)
QMessageBox.information( QMessageBox.information(
self, self,
@@ -1054,5 +849,4 @@ class LoginDialog(QDialog):
if not ok: if not ok:
return "", "" return "", ""
return (cfg.config("username", ""), return cfg.config("username", ""), cfg.config("password", "") # nosec B106
cfg.config("password", "")) # nosec B106
+394 -711
View File
File diff suppressed because it is too large. Load diff
+15 -17
View File
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os.path from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QUrl
from qgis.PyQt.QtGui import QIcon, QDesktopServices
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from qgis.core import Qgis from qgis.core import Qgis
from qgis.PyQt.QtCore import QCoreApplication, QSettings, QTranslator, QUrl
from qgis.PyQt.QtGui import QDesktopServices, QIcon
from qgis.PyQt.QtWidgets import QAction, QDialog, QMenu, QToolButton
from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .amcr_tools import load_amcr_data, login_to_api from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog
import os.path
class AmcrViewer: class AmcrViewer:
@@ -30,7 +29,7 @@ class AmcrViewer:
locale_path = os.path.join( locale_path = os.path.join(
self.plugin_dir, self.plugin_dir,
'i18n', 'i18n',
f'AmcrViewer_{locale}.qm' 'AmcrViewer_{}.qm'.format(locale)
) )
# Install the translator if a translation file # Install the translator if a translation file
@@ -42,7 +41,7 @@ class AmcrViewer:
# Initialize internal state # Initialize internal state
self.actions = [] self.actions = []
self.menu = self.tr('&AMČR Viewer') self.menu = self.tr(u'&AMČR Viewer')
self.first_start = None self.first_start = None
def tr(self, message): def tr(self, message):
@@ -91,7 +90,7 @@ class AmcrViewer:
""" """
# Define paths for action-specific icons # Define paths for action-specific icons
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png') icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
icon_pas_path = os.path.join(self.plugin_dir, 'sn.png') icon_pas_path = os.path.join(self.plugin_dir, 'akce.png')
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png') icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png') icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png')
@@ -103,7 +102,7 @@ class AmcrViewer:
# custom dropdown menu # custom dropdown menu
self.action_download_akce = self.add_action( self.action_download_akce = self.add_action(
icon_path=icon_akce_path, icon_path=icon_akce_path,
text=self.tr('Stáhnout data akcí | AMČR Viewer'), text=self.tr(u'Stáhnout data akcí | AMČR Viewer'),
callback=lambda checked=False: self.run_download('akce'), callback=lambda checked=False: self.run_download('akce'),
parent=self.iface.mainWindow(), parent=self.iface.mainWindow(),
add_to_menu=False, add_to_menu=False,
@@ -113,9 +112,8 @@ class AmcrViewer:
self.action_download_pas = self.add_action( self.action_download_pas = self.add_action(
icon_path=icon_pas_path, icon_path=icon_pas_path,
text=self.tr('Stáhnout data samostatných nálezů | AMČR Viewer'), text=self.tr(u'Stáhnout data samostatných nálezů | AMČR Viewer'),
callback=lambda checked=False: self.run_download( callback=lambda checked=False: self.run_download('pas'),
'samostatny_nalez'),
parent=self.iface.mainWindow(), parent=self.iface.mainWindow(),
add_to_menu=False, add_to_menu=False,
add_to_toolbar=False add_to_toolbar=False
@@ -124,7 +122,7 @@ class AmcrViewer:
self.action_download_lokality = self.add_action( self.action_download_lokality = self.add_action(
icon_path=icon_lokality_path, icon_path=icon_lokality_path,
text=self.tr('Stáhnout data lokalit | AMČR Viewer'), text=self.tr(u'Stáhnout data lokalit | AMČR Viewer'),
callback=lambda checked=False: self.run_download('lokalita'), callback=lambda checked=False: self.run_download('lokalita'),
parent=self.iface.mainWindow(), parent=self.iface.mainWindow(),
add_to_menu=False, add_to_menu=False,
@@ -133,8 +131,8 @@ class AmcrViewer:
self.plugin_menu.addAction(self.action_download_lokality) self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action( self.action_login_dialog = self.add_action(
icon_path=icon_pas_path, icon_path=icon_akce_path,
text=self.tr('Přihlásit se | AMČR Viewer'), text=self.tr(u'Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(), callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(), parent=self.iface.mainWindow(),
add_to_menu=False, add_to_menu=False,
@@ -144,7 +142,7 @@ class AmcrViewer:
self.action_amcr_help = self.add_action( self.action_amcr_help = self.add_action(
icon_path=icon_amcr_help_path, icon_path=icon_amcr_help_path,
text=self.tr('Nápověda AMČR Help | AMČR Viewer'), text=self.tr(u'Nápověda AMČR Help | AMČR Viewer'),
callback=lambda checked=False: self.open_help(), callback=lambda checked=False: self.open_help(),
parent=self.iface.mainWindow(), parent=self.iface.mainWindow(),
add_to_menu=False, add_to_menu=False,
File diff suppressed because it is too large. Load diff
+4 -14
View File
@@ -8,7 +8,7 @@ name=AMČR Viewer
qgisMinimumVersion=3.44.0 qgisMinimumVersion=3.44.0
qgisMaximumVersion=4.99.0 qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data. description=Viewing and downloading the AMČR data.
version=2.1.2 version=2.0.2
author=David Spáčil author=David Spáčil
email=spacil@arub.cz email=spacil@arub.cz
@@ -23,19 +23,9 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
hasProcessingProvider=no hasProcessingProvider=no
# Uncomment the following line and add your changelog: # Uncomment the following line and add your changelog:
changelog= changelog=
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.1.1 Aktualizace na verzi 2.0.0 výrazně mění chování pluginu. Před updatem je doporučeno přečíst si seznam změn níže.
v2.1.2 (2026-09-01) Version 2.0.0 changes the plugin behavior dramatically. It is advised to check the changelog below.
* Qt6 compatibility Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.0.0
* Code clean-up
v2.1.1 (2026-09-01)
* Added download of Individual finds (PAS), including a dedicated menu entry
* Added filtering by date
* Added filtering of project fieldwork events and a Project column in the attribute table
* Codelists are now built from the Digiarchiv API as well, not only from OAI-PMH; persons come from its facets by role (finder, fieldwork leader)
* Updated the bundled codelist file
* Filter dialog is now scrollable
* Empty API results are now diagnosable from the Messages panel: the query URL and API errors are logged
* Code clean-up
v2.0.2 (2026-06-13) v2.0.2 (2026-06-13)
* Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49) * Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49)
v2.0.1 (2026-06-10) v2.0.1 (2026-06-10)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

+7 -23
View File
@@ -9,7 +9,7 @@
id="svg1" id="svg1"
xml:space="preserve" xml:space="preserve"
sodipodi:docname="icon.svg" sodipodi:docname="icon.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -24,12 +24,12 @@
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:zoom="22.627417" inkscape:zoom="22.627417"
inkscape:cx="0.57452426" inkscape:cx="6.8059028"
inkscape:cy="15.556349" inkscape:cy="14.606174"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1131" inkscape:window-height="1009"
inkscape:window-x="0" inkscape:window-x="1912"
inkscape:window-y="0" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs inkscape:current-layer="layer1" /><defs
id="defs1"><clipPath id="defs1"><clipPath
@@ -96,20 +96,4 @@
d="M 3.3486328 2.4970052 L 3.3486328 3.8323242 L 2.7424683 3.8323242 L 4.2085286 5.738151 L 5.674589 3.8323242 L 5.0684245 3.8323242 L 5.0684245 2.4970052 L 3.3486328 2.4970052 z " d="M 3.3486328 2.4970052 L 3.3486328 3.8323242 L 2.7424683 3.8323242 L 4.2085286 5.738151 L 5.674589 3.8323242 L 5.0684245 3.8323242 L 5.0684245 2.4970052 L 3.3486328 2.4970052 z "
inkscape:export-filename="path32.png" inkscape:export-filename="path32.png"
inkscape:export-xdpi="3000" inkscape:export-xdpi="3000"
inkscape:export-ydpi="3000" /><rect inkscape:export-ydpi="3000" /></g></svg>
style="opacity:1;fill:#85140e;fill-opacity:1;stroke:none;stroke-width:0.414999;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
id="rect1"
width="1.672105"
height="1.2511554"
x="-4.501821"
y="4.2814822" /><text
xml:space="preserve"
style="font-size:0.705556px;font-family:'Open Sans';-inkscape-font-specification:'Open Sans';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;opacity:1;fill:#85140e;fill-opacity:1;stroke:none;stroke-width:0.414999;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:stroke fill markers"
x="-2.6360023"
y="5.1136947"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
style="font-size:0.705556px;fill:#85140e;fill-opacity:1;stroke-width:0.415"
x="-2.6360023"
y="5.1136947">lokality</tspan></text></g></svg>

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB