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
20 changed files with 976 additions and 2326 deletions

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ Nepotřebné body můžeš smazat. Komentáře (<!-- ... -->) se v PR nezobrazuj
## Kontrolní seznam
- [ ] Změny jsou v souladu se stylem projektu (viz `AGENTS.md`)
- [ ] Při změně funkcí povýšena verze v `amcr_viewer/metadata.txt` (+ `changelog`) a v `CITATION.cff` (`version`, `date-released`)
- [ ] Při změně funkcí povýšena verze v `amcr_viewer/metadata.txt` a doplněn `changelog`
- [ ] Otestováno v QGIS (min. podporovaná verze 3.44)
- [ ] PR míří do správné cílové větve
- [ ] Větev odpovídá konvenci (`feat/ fix/ docs/ chore/<téma>`, AI `agents/<jméno>/<téma>`)
-184
View File
@@ -1,184 +0,0 @@
name: Code Quality
# Pouští při každém PR tytéž kontroly, které se dosud dělaly ručně:
#
# * co spouští plugins.qgis.org při uploadu (bandit, detect-secrets,
# flake8, analýza souborů) – https://plugins.qgis.org/docs/security-scanning
# * oficiální kontrolu kompatibility s Qt6 (pyqgis4-checker)
# * skutečné načtení pluginu v QGIS 3.44 (Qt5) i v QGIS 4 (Qt6)
# * sestavení ZIPu, který si recenzent stáhne a nainstaluje přímo z PR
#
# CodeQL a GitGuardian běží zvlášť, nastavené na úrovni organizace –
# tady se schválně neduplikují.
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
env:
# Verze se drží napevno, aby se výsledek nezměnil sám od sebe. Výchozí
# sada pravidel ruffu se mezi verzemi mění; povýšení je vědomý krok.
BANDIT: bandit==1.9.4
DETECT_SECRETS: detect-secrets==1.5.0
FLAKE8: flake8==7.3.0
RUFF: ruff==0.16.5
jobs:
# --------------------------------------------------------------------
# 1. Statické kontroly – běží první, protože trvají desítky sekund
# --------------------------------------------------------------------
lint:
name: Lint a bezpečnost
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install tools
run: pip install "$BANDIT" "$DETECT_SECRETS" "$FLAKE8" "$RUFF"
# Hygiena repozitáře: BOM, přímé importy z PyQt5/PyQt6, spustitelná
# práva a podezřelé typy souborů. Padá jako první, protože BOM
# zneviditelní soubor pro kontrolu níž.
- name: Source hygiene
run: python3 tests/check_sources.py
# Blokující kontrola na plugins.qgis.org
- name: Bandit
run: bandit -r amcr_viewer/
# Blokující kontrola na plugins.qgis.org.
# --all-files je podstatné: bez něj detect-secrets prohledá jen
# soubory sledované gitem a nesledovaný soubor tiše přeskočí.
- name: detect-secrets
run: |
detect-secrets scan --all-files amcr_viewer/ > vysledek.json
python3 -c "
import json, sys
nalezy = json.load(open('vysledek.json'))['results']
if nalezy:
print(json.dumps(nalezy, indent=2))
sys.exit(1)
print('detect-secrets: bez nálezů')
"
# Na plugins.qgis.org je informativní, tady blokuje. Bez konfigurace,
# tj. se stejnými výchozími pravidly jako scanner.
- name: Flake8
run: flake8 --isolated amcr_viewer/
# Nad rámec plugins.qgis.org; konfigurace v pyproject.toml
- name: Ruff
run: ruff check .
# --------------------------------------------------------------------
# 2. Kompatibilita s Qt6 – oficiální skript z QGISu
# --------------------------------------------------------------------
qt6:
name: Kompatibilita s Qt6
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Pozor: skript končí kódem 0 i když něco najde, výsledek je jen
# v logu. Prázdný log (samotná hlavička) znamená čisto.
- name: pyqgis4-checker
run: |
docker run --rm --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 .
echo "--- pyqt6_checker.log ---"
cat pyqt6_checker.log
nalezu=$(grep -v '=== dry_run mode | Start Logs ===' pyqt6_checker.log | wc -l)
if [ "$nalezu" -ne 0 ]; then
echo "::error::pyqgis4-checker nahlásil nálezy, viz log výše"
exit 1
fi
# --------------------------------------------------------------------
# 3. Načtení pluginu ve skutečném QGIS, v obou podporovaných verzích
# --------------------------------------------------------------------
qgis:
name: Smoke test (QGIS ${{ matrix.qgis }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# ltr = 3.44 na Qt5, stable = 4.x na Qt6. Tagy se posouvají
# schválně: chceme vědět, že plugin drží krok s aktuálním QGISem.
qgis: [ltr, stable]
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Smoke test
run: |
docker run --rm -v "$PWD:/work:ro" -w /work \
--user "$(id -u):$(id -g)" -e HOME=/tmp \
"qgis/qgis:${{ matrix.qgis }}" python3 tests/smoke_test.py
# --------------------------------------------------------------------
# 4. ZIP k instalaci – stejný postup jako v release_plugin.yml
# --------------------------------------------------------------------
package:
name: Balíček pluginu
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Zip plugin
run: zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# Verze v CITATION.cff se povyšuje ručně spolu s metadata.txt a snadno
# se zapomene – tag by pak nesl v citaci jinou verzi než plugin
- name: Verify CITATION.cff version
run: |
plugin=$(sed -n 's/^version=//p' amcr_viewer/metadata.txt \
| tr -d "\r\"' ")
citace=$(sed -n 's/^version://p' CITATION.cff \
| tr -d "\r\"' ")
echo "metadata.txt: '$plugin', CITATION.cff: '$citace'"
if [ -z "$plugin" ] || [ "$plugin" != "$citace" ]; then
echo "::error file=CITATION.cff::verze '$citace' neodpovídá" \
"metadata.txt ('$plugin')"
exit 1
fi
# Kontrola obsahu ZIPu: povinné soubory jsou uvnitř, git soubory ne.
- name: Verify archive contents
run: |
unzip -l amcr_viewer.zip
for soubor in amcr_viewer/metadata.txt amcr_viewer/__init__.py \
amcr_viewer/LICENSE; do
unzip -l amcr_viewer.zip | grep -qF " $soubor" \
|| { echo "::error::v ZIPu chybí $soubor"; exit 1; }
done
if unzip -l amcr_viewer.zip | grep -qE '\.git'; then
echo "::error::v ZIPu jsou git soubory"
exit 1
fi
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: amcr_viewer-plugin
path: amcr_viewer.zip
if-no-files-found: error
+8 -22
View File
@@ -1,26 +1,15 @@
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:
push:
tags:
- 'v*'
permissions:
contents: write
release:
types: [published]
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
# 1. Stáhne kód z tagu, který běh spustil
# 1. Stáhne kód z repozitáře
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -31,14 +20,11 @@ jobs:
# -r = rekurzivně, -x = ignorovat skryté git soubory
zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# 3. Založí koncept releasu i s přílohou
# Tagy s pomlčkou (v2.0.0-alpha.1) se označí jako pre-release.
- name: Create draft release with asset
# 3. Nahraje ZIP k Releasu
- name: Upload Release Asset
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
if: startsWith(github.ref, 'refs/tags/')
with:
files: amcr_viewer.zip
draft: true
name: AMCR Viewer ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -120
View File
@@ -83,78 +83,17 @@ Další pravidla:
definice pole (`QgsField`), naplnění hodnoty z dokumentu, překlad hlavičky
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
- Verze pluginu žije v **`amcr_viewer/metadata.txt`** (`version=`).
- **Při každé změně chování / nové funkci** povyš verzi a doplň položku do
`changelog=` v `metadata.txt` (formát `vX.Y.Z (RRRR-MM-DD)` + odrážky).
- Současně povyš i **`CITATION.cff`** v kořeni repozitáře: `version:` na
stejnou verzi jako v `metadata.txt` a `date-released:` na datum releasu.
Oba soubory musí mít stejnou verzi, než se založí tag.
- Datum v changelogu ber z **deterministického zdroje**, ne z paměti, např.
`python -c "import datetime; print(datetime.date.today().isoformat())"`.
- Release se spouští **pushnutím tagu `vX.Y.Z`**, ne publikací releasu
v UI. Workflow `.github/workflows/release_plugin.yml` zabalí složku
`amcr_viewer/` do `amcr_viewer.zip` a založí **koncept** releasu i s touto
přílohou; text se dopíše a release zveřejní ručně. Do ZIPu se nesmí dostat
`.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.
- Release se vytváří publikací GitHub Release; workflow
`.github/workflows/release_plugin.yml` zabalí složku `amcr_viewer/` do
`amcr_viewer.zip` a přiloží ji k releasu. Do ZIPu se nesmí dostat `.git*`
soubory.
## Pull requesty
@@ -179,58 +118,5 @@ flatpak run --command=sh org.qgis.qgis -c \
Plugin se testuje načtením do QGIS (Plugins → Manage and Install Plugins →
Install from ZIP, nebo nasazením složky `amcr_viewer/` do adresáře pluginů
QGIS). **Ruční test v QGIS nic nenahrazuje** – automatické kontroly ověřují,
že se plugin načte a že projde kontrolami kvality, ne že dělá správnou věc.
### Automatické kontroly
Workflow `.github/workflows/code_quality.yml` pouští při každém PR do `main`
tohle:
| job | co dělá |
|---|---|
| **Lint a bezpečnost** | `check_sources.py`, bandit, detect-secrets, flake8, ruff |
| **Kompatibilita s Qt6** | `pyqgis4-checker` v dockeru |
| **Smoke test** | `smoke_test.py` v `qgis/qgis:ltr` i `qgis/qgis:stable` |
| **Balíček pluginu** | ověří shodu verze v `CITATION.cff` a `metadata.txt`, sestaví ZIP, ověří obsah, přiloží jako artefakt |
Smoke test běží v obou podporovaných řadách: `ltr` je QGIS 3.44 na Qt5,
`stable` je QGIS 4.x na Qt6.
Artefakt z posledního jobu se dá stáhnout ze stránky běhu a rovnou
nainstalovat přes *Install from ZIP* – recenzent nemusí nic balit ručně.
Totéž lokálně:
```sh
pip install bandit detect-secrets flake8 ruff
python3 tests/check_sources.py
bandit -r amcr_viewer/
detect-secrets scan --all-files amcr_viewer/
flake8 --isolated amcr_viewer/
ruff check .
# smoke test v obou verzích QGIS (docker, bez instalace čehokoli)
for tag in ltr stable; do
docker run --rm -v "$PWD:/work:ro" -w /work \
--user "$(id -u):$(id -g)" -e HOME=/tmp \
"qgis/qgis:$tag" python3 tests/smoke_test.py
done
```
Na co si dát pozor:
- **`pyqgis4-checker` končí kódem 0, i když něco najde** – výsledek je jen
v logu. Workflow proto kontroluje, že log obsahuje jen hlavičku.
- **`detect-secrets` bez `--all-files` prohledá jen soubory sledované
gitem** a o nesledovaném souboru mlčí. Vypadá to jako čistý výsledek.
- **Flake8 běží bez konfigurace** (`--isolated`), tedy se stejnými
výchozími pravidly jako scanner na plugins.qgis.org. Do balíčku nepatří
`.flake8`, `.bandit` ani `.secrets.baseline`: scanner by plugin označil
jako „Validated (configured)“ a nález je lepší opravit v kódu.
Konfigurace ruffu je v kořenovém `pyproject.toml` – ruff se do balíčku
pluginu nedistribuuje.
Viz https://plugins.qgis.org/docs/security-scanning/config-files
- **Verze nástrojů jsou v workflow napevno.** Výchozí sada pravidel ruffu se
mezi verzemi mění, takže bez pinu by CI začalo padat samo od sebe.
QGIS). Automatizované testy zatím repozitář neobsahuje – změny ověřuj ručně
v QGIS na podporované verzi.
+2 -2
View File
@@ -25,5 +25,5 @@ abstract: >-
the Digital archive of the Archaeological Map of the
Czech Republic (https://digiarchiv.aiscr.cz/).
license: GPL-3.0
version: '2.1.4'
date-released: '2026-10-01'
version: '2.0.0'
date-released: '2026-06-05'
+6 -1
View File
@@ -1 +1,6 @@
@AGENTS.md
# CLAUDE.md
Pokyny pro tento repozitář jsou v [`AGENTS.md`](./AGENTS.md).
Tento soubor je záměrně jen odkaz, aby existoval jeden zdroj pravdy a obsah se
nerozjel. Cokoli platí pro AI agenty v tomto projektu, najdeš v `AGENTS.md`.
+162 -411
View File
@@ -1,431 +1,182 @@
# AMČR Viewer — QGIS plugin
# AMCR Viewer: QGIS Plugin Documentation
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![QGIS 3.44 – 4.x](https://img.shields.io/badge/QGIS-3.44%20%E2%80%93%204.x-589632.svg)](https://qgis.org/)
[![Code quality](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/actions/workflows/code_quality.yml/badge.svg)](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/actions/workflows/code_quality.yml)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18609813.svg)](https://doi.org/10.5281/zenodo.18609813)
**AMČR Viewer** queries the Digital Archive of the Archaeological Map of the
Czech Republic (AMČR) and turns the result into ordinary QGIS vector layers.
It removes the manual export/import round trip: you filter the archive from
inside QGIS and the matching records arrive as point, line and polygon layers
with a full attribute table.
**Platform:** QGIS 3.44.0–4.99.0
| | |
**Module Type:** Data Acquisition & Visualization
**Source Data:** Archaeological Map of the Czech Republic (AIS CR)
---
## 1. Overview
**AMCR Viewer** is a QGIS plugin designed to facilitate direct access to the Digital Archive of the Archaeological Map of the Czech Republic (AMČR). It allows researchers to **query, retrieve, and visualize *Fieldwork events* and *Sites* data (metadata and geometry) directly within the GIS environment**, eliminating the need to manually export data from the web interface. Both *Fieldwork events* and *Sites* layers may optionally include component-level data (period and activity area) embedded directly in the attribute table. The plugin supports both **anonymous (public) access** and **authenticated access** for users with an AMČR account.
### Key Features
* **Spatial Querying:** Option to filter records based on the current map canvas extent (Bounding Box).
* **Advanced Attribute Filtering:** Supports multi-criteria filtering using controlled vocabularies.
* **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.
* **Authenticated Access:** Users with an AMČR account can log in to access non-public records.
## 2. Installation Guide
**Install the latest version of the plugin from the QGIS plugin repository.**
**OR** (*in case you need older version*)
1. *Obtain the [plugin distribution package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases) (ZIP archive containing the `amcr_viewer` directory).*
2. *Launch QGIS.*
3. *Navigate to Plugins → Manage and Install Plugins...*
4. *Select the Install from ZIP tab.*
5. *Locate the source ZIP file and click Install Plugin.*
6. *Upon successful installation, the AMCR Viewer button will appear in the toolbar.*
## 3. User Manual
### 3.1 Authentication (Optional)
By default, the plugin accesses only publicly available records (accessibility = anonymous). To access non-public data, log in using your AMČR account:
* Click the dropdown arrow on the AMCR Viewer toolbar button and select **Přihlásit se**.
* Enter your e-mail and password. Credentials are encrypted and stored securely in the **QGIS Authentication Manager** (DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
* Stored credentials are reused automatically across sessions. To update or remove them, open the login dialog again.
### 3.2 Data Retrieval
To initiate a search query, click either the **Stáhnout data akcí** or the **Stáhnout data lokalit** option from the dropdown menu. The filter dialog provides the following options. Shown options vary based on the chosen tool.
* **Spatial Filter:** *Checkbox "Omezit vyhledávání rozsahem okna":* If checked, the query is restricted to the geographical area currently visible in the QGIS canvas. If unchecked, the query searches the entire database (use with caution regarding data volume).
* **Positive findings only:** If checked, only *PIANs* belonging to Documentation units marked as "Type of evidence" = "positive" are included. *(Fieldwork events only.)*
* **Attribute Filters:**
* The dialog uses "Picker" widgets for controlled vocabularies (common: Region, District, Cadastral area, Period, Activity Area, *PIAN* accuracy, Accessibility; *events* related: Organisation, Researcher, Event type; *sites* related: Site type and class, Level of confidence, State of preservation).
* Click **Vybrat...** to open a searchable selection window. Multiple values can be selected simultaneously (Logic: OR).
* **Codelists (Hesláře):**
* Controlled vocabularies are downloaded from the AMČR OAI-PMH API and cached locally in `codelists/heslar.csv`.
* To refresh all codelists, click the **Aktualizovat hesláře 🔄** button in the filter dialog. This runs as a background task and may take a few minutes.
* **Components:** Check **Načíst komponenty** to include period and activity area data directly in the output layers.
> ⚠ When components are loaded, spatial features are duplicated — each feature corresponds to one component. Spatial analyses (areas, counts) may be inaccurate.
* If no filter is used, all accessible Fieldwork events/PIANs are returned (the number of records is capped at 20 000; it is advisable to set at least one filter).
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.3 Layer Structure & Attributes
Upon successful retrieval, the plugin generates up to three temporary memory layers:
1. **AMCR\_[Akce|Lokalita]\_Polygony**
2. **AMCR\_[Akce|Lokalita]\_Linie**
3. **AMCR\_[Akce|Lokalita]\_Body**
Layers are only created if the query returns features of the corresponding geometry type. All layers share the same attribute schema.
#### 3.3.1 Common fields
| Field | Description |
| --- | --- |
| **Source data** | [Digital Archive AMČR](https://digiarchiv.aiscr.cz/) (AIS CR) |
| **Supported QGIS** | 3.44.0 – 4.99.0 (Qt 5 and Qt 6) |
| **Output** | temporary `memory` layers, S-JTSK / **EPSG:5514** |
| **Access** | anonymous by default; optional login for non-public records |
| **UI language** | Czech |
| **Licence** | GPL-3.0 |
| pian | PIAN (spatial identifier) ID |
| presnost | Spatial deviation \[units/tens/hundreds of meters/defined by cadastre\] |
| pian\_typ | \[point/line/polygon\] |
| dj | Documentation unit ID |
| typ\_dj | \[trench/event part/whole event/cadastral territory\] |
| definicni\_body | Feature centroid in WGS-84 coordinate system |
| akce / lokalita | Fieldwork event / Site ID |
| odkaz\_do\_digiarchivu | Link to the record in the Digital Archive |
| okres | District |
| katastr | Main cadastral area |
| dalsi\_katastry | Other cadastral areas, if the event extends beyond the main cadastre |
| Přístupnost | Record accessibility \[A/B/C/D\] |
---
#### 3.3.2 Fields related to *Fieldwork events*
## 1. What it can download
The plugin covers three AMČR record types. Each has its own menu entry, its
own set of filters and its own attribute table.
| Entity | Menu entry | What it is |
| --- | --- | --- |
| **Fieldwork events** (`akce`) | *Stáhnout data akcí* | Records of archaeological finds and observations tied to a place, a responsible body and a time of execution. |
| **Sites** (`lokalita`) | *Stáhnout data lokalit* | Records tied to a site, its characteristic archaeological manifestation and presumed function. |
| **Individual finds** (`samostatny_nalez`) | *Stáhnout data samostatných nálezů* | Records of individual movable finds reported through **AMČR-PAS**, the portal for amateur collaborators. |
Fieldwork events and Sites can additionally carry **component** data (period
and activity area) directly in the attribute table. Individual finds have no
components — period and dating are attributes of the find itself.
### Key features
* **Spatial querying** — restrict the query to the current map canvas extent.
* **Multi-criteria attribute filtering** driven by AMČR controlled
vocabularies (*hesláře*), with a searchable multi-select picker per filter.
* **Date range filtering** for fieldwork start/end and for the date of finding.
* **Automatic geometry retrieval**, split into Point, Line and Polygon layers
and reprojected to S-JTSK.
* **Human-readable labels** — internal codes (`HES-xxxxxx`) are translated via
the AIS CR translation dictionary.
* **Authenticated access** — an AMČR account unlocks non-public records;
credentials are stored encrypted in the QGIS Authentication Manager.
---
## 2. Installation
### From the QGIS plugin repository (recommended)
*Plugins → Manage and Install Plugins… → search for* **AMČR Viewer** *→
Install*.
### From a ZIP archive (older versions, or a build from source)
1. Download a [release package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases)
(a ZIP containing the `amcr_viewer` directory).
2. In QGIS go to *Plugins → Manage and Install Plugins… → Install from ZIP*.
3. Select the archive and click *Install Plugin*.
Every successful CI run also publishes a ready-to-install `amcr_viewer.zip`
as a build artifact, which is handy for testing a branch before release.
After installation the **AMČR Viewer** button appears in the toolbar as a
dropdown.
### Requirements
The plugin needs the **`requests`** library. It ships with the QGIS installers
for Windows and macOS. On Linux distribution packages it may have to be
installed separately (e.g. `python3-requests`).
---
## 3. User manual
### 3.1 Toolbar and menu
The toolbar button is a dropdown; the default action is *Stáhnout data akcí*.
| Menu entry | Action |
| Field | Description |
| --- | --- |
| *Stáhnout data akcí* | Opens the filter dialog for Fieldwork events. |
| *Stáhnout data samostatných nálezů* | Opens the filter dialog for Individual finds. |
| *Stáhnout data lokalit* | Opens the filter dialog for Sites. |
| *Přihlásit se* | Opens the login dialog (see 3.2). |
| *Nápověda AMČR Help* | Opens the online documentation in a browser. |
| akce\_lokalizace | Verbal description of the event location |
| vedouci | Main fieldwork manager |
| organizace | Organisation conducting the research |
| specifikace\_data | \[exact date/exact years/sometime in years\] |
| zahajeni | Event start date |
| ukonceni | Event end date |
| hlavni\_typ | Primary research method |
| vedlejsi\_typ | Secondary research method |
| zjisteni | Did the research reveal archaeological contexts? \[positive/negative\] |
| nahrazuje\_NZ | Replaces a fieldwork report? \[yes/no\] |
### 3.2 Authentication (optional)
#### 3.3.3 Fields related to *Sites*
By default the plugin sees only publicly accessible records. Logging in with
an AMČR account extends the result set to everything the account is allowed
to see.
* The credentials are **verified against the API before they are stored** —
a wrong password never reaches the Authentication Manager. If the server is
unreachable, the plugin offers to store them unverified.
* They are then saved encrypted in the **QGIS Authentication Manager** (DPAPI
on Windows, Keychain on macOS, encrypted SQLite on Linux). QGIS will ask for
its master password.
* Stored credentials are reused across QGIS sessions. If the session cookie
expires mid-download, the plugin re-authenticates automatically and repeats
the request.
* Reopening the login dialog lets you change the e-mail (leave the password
blank to keep the stored one) or remove the credentials entirely
(*Odebrat uložené přihlašovací údaje*).
### 3.3 The filter dialog
Filters of different categories are combined with **AND**; multiple values
inside one filter are combined with **OR**. A filter left empty means "no
restriction". Click *Vybrat…* to open a searchable, checkable list.
#### Availability per entity
| Filter (Czech UI label) | Events | Sites | Ind. finds | API parameter |
| --- | :---: | :---: | :---: | --- |
| Omezit vyhledávání rozsahem okna | ✓ | ✓ | ✓ | `loc_rpt` |
| Pouze pozitivní zjištění | ✓ | — | — | `posevidence` |
| Pouze projektové akce | ✓ | — | — | `proj_akce` |
| Kraj | ✓ | ✓ | ✓ | `f_kraj` |
| Okres | ✓ | ✓ | ✓ | `f_okres` |
| Katastr | ✓ | ✓ | ✓ | `f_katastr` |
| Přístupnost | ✓ | ✓ | ✓ | `pristupnost` |
| PIAN – přesnost | ✓ | ✓ | — | `f_pian_presnost` |
| Organizace | ✓ | — | ✓ | `f_organizace` |
| Vedoucí výzkumu | ✓ | — | — | `f_vedouci` |
| Typ výzkumu | ✓ | — | — | `f_typ_vyzkumu` |
| Datum — *Zahájení* / *Ukončení* | ✓ | — | — | `akce_datum_zahajeni`, `akce_datum_ukonceni` |
| Lokalita – typ | — | ✓ | — | `f_typ_lokality` |
| Lokalita – druh | — | ✓ | — | `f_druh_lokality` |
| Lokalita – jistota určení | — | ✓ | — | `f_jistota` |
| Lokalita - stav dochování | — | ✓ | — | `f_lokalita_zachovalost` |
| Období | ✓ | ✓ | ✓ | `f_obdobi` |
| Kategorie nálezu | — | — | ✓ | `f_kategorie` |
| Druh nálezu | — | — | ✓ | `f_druh_nalezu` |
| Specifikace nálezu | — | — | ✓ | `f_specifikace` |
| Okolnosti nálezu | — | — | ✓ | `f_nalezove_okolnosti` |
| Nálezce | — | — | ✓ | `f_nalezce` |
| Datum nálezu | — | — | ✓ | `samostatny_nalez_datum_nalezu` |
| Areál | ✓ | ✓ | — | `f_areal` |
| Načíst komponenty | ✓ | ✓ | — | — |
#### Spatial restriction
*Omezit vyhledávání rozsahem okna* is **checked by default**. The canvas
extent is transformed from the project CRS to WGS-84 and sent as a bounding
box. Unchecking it queries the whole database — do so with an attribute
filter in place, otherwise you will hit the record cap (see 4.5).
#### PIAN accuracy has a non-empty default
> ⚠ *PIAN – přesnost* is the one filter that is **pre-selected**. For
> Fieldwork events and Sites the dialog starts with *odchylka jednotky metrů*,
> *odchylka desítky metrů* and *odchylka stovky metrů* checked, so an
> otherwise untouched dialog already sends `f_pian_presnost`. Records
> localised only to a cadastral territory are excluded until you open the
> picker and add that level yourself.
#### Date ranges
Each date block has a *from* and a *to* picker; an empty picker shows
*neomezeno* and means an open bound. The API rejects a one-sided range, so the
plugin substitutes a sentinel (`0001-01-01` / `9999-12-31`) for the empty
side. A block with **both** pickers empty adds no filter at all.
A reversed range (start later than end) is refused when you confirm the
dialog — such a query would come back empty and would be indistinguishable
from a genuinely empty result.
#### Codelists (hesláře)
The controlled vocabularies behind the pickers are cached in
`amcr_viewer/codelists/heslar.csv` and ship with the plugin. Click
**Aktualizovat hesláře 🔄** to rebuild the file from the live APIs; it runs as
a background QGIS task with a progress bar and takes a few minutes.
Most codelists come from the AMČR **OAI-PMH** endpoint. Two are built from
Digiarchiv **search facets** instead, because they are lists of people rather
than a published vocabulary: *Vedoucí výzkumu* (`f_vedouci`, faceted over
fieldwork events) and *Nálezce* (`f_nalezce`, faceted over individual finds).
#### Components
Check **Načíst komponenty** (Events and Sites only) to bring the period and
activity area of each component into the output layer.
> ⚠ With components loaded, spatial features are **duplicated** — one feature
> per component. Areas and feature counts computed on such a layer are
> misleading.
Note that *Období* and *Areál* also act as component filters even when the
box is unchecked: a documentation unit whose components match nothing is
dropped from the result.
### 3.4 Output layers
Up to three temporary `memory` layers are created per download, in **S-JTSK
(EPSG:5514)**:
* `AMCR_Akce_Body` / `_Linie` / `_Polygony`
* `AMCR_Lokalita_Body` / `_Linie` / `_Polygony`
* `AMCR_Samostatný_nález_Body` / `_Linie` / `_Polygony`
A layer is only created if the query actually returned that geometry type.
All layers of one download share the same attribute schema. Field names are
ASCII; the human-readable names visible in the attribute table are QGIS field
aliases.
> Memory layers are **not persistent** — export them (GeoPackage, Shapefile,
> …) before closing the project.
Geometry is taken from the record's S-JTSK WKT when present; otherwise the
WGS-84 fallback is reprojected. Invalid geometries are repaired rather than
dropped.
The tables below group the fields by meaning. In the layer they appear in the
order *common → entity-specific → `pristupnost` → component fields*.
#### Common fields
| Field | Alias | Description |
| --- | --- | --- |
| `pian` | PIAN | Spatial unit (PIAN) identifier. *Events and Sites only.* |
| `presnost` | Přesnost | Spatial accuracy \[units / tens / hundreds of metres / defined by cadastre\]. *Events and Sites only.* |
| `pian_typ` | PIAN – typ | \[point / line / polygon\]. *Events and Sites only.* |
| `dj` | Dokumentační jednotka | Documentation unit identifier. *Events and Sites only.* |
| `typ_dj` | Typ dokumentační jednotky | \[trench / event part / whole event / cadastral territory\]. *Events and Sites only.* |
| `akce` / `lokalita` / `samostatny_nalez` | Akce / Lokalita / Samostatný nález | Record identifier. |
| `definicni_body` | Definiční bod(y) (WGS-84) | Feature centroid(s) in WGS-84. |
| `odkaz_do_digiarchivu` | Odkaz do Digitálního archivu AMČR | Permalink to the record. |
| `okres` | Okres | District. |
| `katastr` | Katastr | Main cadastral area. |
| `dalsi_katastry` | Další katastry | Other cadastral areas. *Always empty for individual finds.* |
| `pristupnost` | Přístupnost | Record accessibility \[A/B/C/D\]. |
#### Fieldwork event fields
| Field | Alias | Description |
| --- | --- | --- |
| `akce_lokalizace` | Akce – lokalizace | Verbal description of the location. |
| `vedouci` | Vedoucí akce | Main fieldwork manager. |
| `organizace` | Organizace | Organisation conducting the research. |
| `specifikace_data` | Specifikace data | \[exact date / exact years / sometime in years\]. |
| `zahajeni` | Datum zahájeni | Start date. |
| `ukonceni` | Datum ukončení | End date. |
| `hlavni_typ` | Hlavní typ | Primary research method. |
| `vedlejsi_typ` | Vedlejší typ | Secondary research methods. |
| `zjisteni` | Zjištění | Whether the **documentation unit** is positive or negative evidence \[Pozitivní / Negativní\]. |
| `nahrazuje_NZ` | Akce – nahrazuje NZ | Replaces a fieldwork report \[Ano / Ne\]. |
| `projekt` | Projekt | Identifier of the related project, if any. |
#### Site fields
| Field | Alias | Description |
| --- | --- | --- |
| `nazev_lokality` | Název lokality | Site name. |
| `popis_lokality` | Popis lokality | Site description. |
| `typ_lokality` | Typ lokality | Site classification by definition method. |
| `druh_lokality` | Druh lokality | Site classification by the nature of the field relics. |
| `zachovalost` | Zachovalost | State of preservation. |
#### Individual find fields
| Field | Alias | Description |
| --- | --- | --- |
| `projekt` | Projekt | Identifier of the related project. |
| `nalezce` | Nálezce | Finder. |
| `datum` | Datum nálezu | Date of finding. |
| `okolnosti` | Nálezové okolnosti | Finding context. |
| `hloubka_cm` | Hloubka (cm) | Depth below surface. |
| `lokalizace` | Lokalizace | Verbal description of the find spot. |
| `obdobi` | Období | Period. |
| `presna_datace` | Přesná datace | Precise dating, if known. |
| `nalez` | Nález | Find class. |
| `material` | Materiál | Find specification / material. |
| `pocet` | Počet předmětů | Number of objects. |
| `poznamka` | Poznámka/bližší popis | Note or closer description. |
| `pred_org` | Předáno organizaci | Organisation the find was handed over to. |
| `evidencni` | Evidenční číslo | Reference number. |
#### Component fields (only with *Načíst komponenty*)
| Field | Alias | Description |
| --- | --- | --- |
| `komponenta` | Komponenta | Component identifier. |
| `komponenta_areal` | Areál | Activity area \[settlement / burial area / field / …\]. |
| `komponenta_obdobi` | Období | Period \[Neolithic / High Middle Ages–Modern Period / …\]. |
### 3.5 When a query returns nothing
Progress and errors are written to the QGIS *Messages* panel, tab **AMČR**
(login goes to **AMČR login**). The log contains the **exact request URL**,
so a suspicious query can be replayed in a browser instead of being
reconstructed from the code. Distinct messages tell apart an empty result, an
API error, a network failure and a result without any geometry.
Only one download can run at a time; starting a second one while the first is
still running is refused with a message.
For a step-by-step tutorial see the
[AMČR documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html)
(Czech only).
---
## 4. Technical notes
The plugin is plain **Python 3** with **`requests`** for HTTP. The GUI is
built through the **`qgis.PyQt`** compatibility layer rather than importing
`PyQt5`/`PyQt6` directly, which is what lets a single source tree run on both
QGIS 3.44 (Qt 5) and QGIS 4 (Qt 6). Every enum is referenced in its scoped
form (`Qt.CheckState.Checked`, …), as required by Qt 6.
### 4.1 Repository layout
```
amcr_viewer/ the plugin package (this is what gets zipped)
__init__.py classFactory() entry point for QGIS
amcr_viewer.py toolbar/menu integration, login flow, dispatch
amcr_dialog.py AmcrFilterDialog, FilterableSelectionDialog,
LoginDialog, UpdateCodelistsTask
amcr_tools.py API access, pagination, parsing, layer building
amcr_codelists.py codelist download and CSV cache
codelists/heslar.csv cached controlled vocabularies
i18n/ Qt translation files
*.png toolbar and menu icons
metadata.txt plugin metadata and changelog
tests/
check_sources.py source hygiene checks (no QGIS needed)
smoke_test.py loads the plugin in a real, headless QGIS
.github/workflows/ CI (code quality, release packaging)
pyproject.toml ruff configuration
AGENTS.md contributor and AI-agent guidelines
```
### 4.2 API endpoints
| Purpose | Endpoint | Notes |
| --- | --- | --- |
| Login | `POST https://digiarchiv.aiscr.cz/api/user/login` | Returns a session cookie. Errors arrive with HTTP 200 and an `error` key. |
| Search | `GET https://digiarchiv.aiscr.cz/api/search/query` | `entity=akce\|lokalita\|samostatny_nalez\|pian`, `mapa=true`, paginated. |
| Translations | `GET https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json` | Code → Czech label; cached in memory for the session. |
| Codelists | `GET https://api.aiscr.cz/2.2/oai` | OAI-PMH `ListRecords`, with resumption tokens. |
### 4.3 Processing pipeline
1. **Metadata** are paged in batches of **500** records, deduplicated by
`ident_cely`, until the reported `numFound` is reached or the cap is hit.
2. Records **without geometry are skipped**; the rest are expanded into
documentation units and — if requested — into components.
3. **Geometries** (PIAN) are fetched separately in batches of **200**
identifiers, to stay under URL length limits.
4. Features are built, reprojected to EPSG:5514, sorted by geometry type and
added to the project in a single batch per layer.
### 4.4 Data persistence
* **Codelists** — `amcr_viewer/codelists/heslar.csv`, rewritten only when the
user asks for an update.
* **Credentials** — QGIS Authentication Manager; the config ID is kept in
`QSettings` under `amcr_viewer/auth_config_id`.
* **Layers** — `memory` only, lost when QGIS closes.
### 4.5 Limits
* **20 000 records** per query (safety cap; QGIS would otherwise freeze).
* **500** records per metadata request, **200** identifiers per geometry
request.
* With components loaded, one output feature equals one component, so a
single PIAN can appear several times in the layer.
---
## 5. Development
Contributor rules, branch naming and the manual QGIS test checklist live in
[`AGENTS.md`](./AGENTS.md).
Every pull request runs
[`.github/workflows/code_quality.yml`](.github/workflows/code_quality.yml),
which mirrors what plugins.qgis.org checks on upload and adds what it does
not:
| Job | What it does |
| Field | Description |
| --- | --- |
| **Lint a bezpečnost** | `tests/check_sources.py`, bandit, detect-secrets, flake8, ruff |
| **Kompatibilita s Qt6** | `pyqgis4-checker` in dry-run mode |
| **Smoke test** | loads the plugin in headless QGIS — both `ltr` (Qt 5) and `stable` (Qt 6) |
| **Balíček pluginu** | builds `amcr_viewer.zip`, asserts its contents, uploads it as an artifact |
| nazev\_lokality | Site name |
| popis\_lokality | Site description |
| typ\_lokality | Site classification by definition method |
| druh\_lokality | Site classification by the nature of identified field relics |
| zachovalost | Site preservation state |
Reproducing them locally:
#### 3.3.4 Component fields (only when *Načíst komponenty* is checked)
```bash
python3 tests/check_sources.py
ruff check .
flake8 --isolated amcr_viewer/
bandit -r amcr_viewer/
docker run --rm -v "$PWD:/work:ro" -w /work --user "$(id -u):$(id -g)" \
-e HOME=/tmp qgis/qgis:stable python3 tests/smoke_test.py
```
| Field | Description |
| --- | --- |
| komponenta | Component ID |
| komponenta\_areal | Activity area \[settlement/burial area/field/…\] |
| komponenta\_obdobi | Period \[Neolithic/High Middle Ages–Modern Period/…\] |
---
## 4. Technical Architecture
## 6. Links and resources
The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
* [AMČR / Digiarchiv documentation](https://amcr-help.aiscr.cz/) (Czech only)
* [AMČR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html)
(Czech only)
* [AMČR-PAS](https://amcr-info.aiscr.cz/amcr-pas/) — the amateur collaborator
portal behind the Individual finds records
* [Import/Export. Pluginy propojující QGIS s AMČR \[poster\]](https://zenodo.org/records/20504909)
(Czech only; describes v1.3.2)
> **Note:** The `requests` library is bundled with the QGIS installers for Windows and macOS. On Linux (distribution packages), it may need to be installed separately (e.g. `python3-requests`).
## Citing
### 4.1 File Structure
Cite the plugin using [`CITATION.cff`](./CITATION.cff) or the concept DOI
[10.5281/zenodo.18609813](https://doi.org/10.5281/zenodo.18609813), which
always resolves to the latest release.
* `amcr_viewer.py`: Entry point; handles GUI integration, toolbar/menu setup, and login flow.
* `amcr_dialog.py`: Manages the UI logic, including `AmcrFilterDialog`, `FilterableSelectionDialog`, and `LoginDialog`.
* `amcr_tools.py`: Core logic module. Handles authentication, API requests, pagination, data parsing, and vector layer generation.
* `amcr_codelists.py`: Manages local caching of controlled vocabularies (`codelists/heslar.csv`) downloaded via OAI-PMH.
## Licence
### 4.2 Data Flow & API Integration
GPL-3.0 — see [`LICENSE`](./LICENSE).
The plugin interacts with the following endpoints:
1. **Login API:**
* Endpoint: `https://digiarchiv.aiscr.cz/api/user/login`
* Method: `POST`
* Returns a session cookie used for subsequent authenticated requests.
* Credentials are stored in the QGIS Authentication Manager; the session is restored automatically if it expires mid-download.
2. **Search API (Solr):**
* Endpoint: `https://digiarchiv.aiscr.cz/api/search/query`
* Method: `GET`
* Parameters: `entity=akce|lokalita|pian`, `rows/page` (pagination), `mapa=true`.
* Logic: Paginated in batches of 500 records (metadata) and 200 records (geometries). A safety cap of 20 000 records is enforced.
3. **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. Cached in memory for the session.
4. **Codelists API (OAI-PMH):**
* Endpoint: `https://api.aiscr.cz/2.2/oai`
* Used for downloading controlled vocabularies (periods, regions, organisations, etc.) on demand.
### 4.3 Data Persistence
* **Vocabularies:** Stored in `codelists/heslar.csv`; updated on user request via the background task.
* **Layers:** Output layers are created as `memory` layers. They are non-persistent and will be lost if QGIS is closed without saving.
### 4.4 Constraints
* **Record Limit:** A safety cap of 20 000 records is enforced.
* **Batch Processing:** Geometry fetching is batched (200 IDs per request) to comply with URL length limitations and server load balancing.
* **Component duplication:** When components are loaded, each output feature corresponds to one component rather than one documentation unit. A single PIAN may therefore appear multiple times in the layer.
## 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).
* [Import/Export. Pluginy propojující QGIS s AMČR \[poster\]](https://zenodo.org/records/20504909) (only in Czech; valid for v1.3.2).
+1
View File
@@ -31,5 +31,6 @@ def classFactory(iface): # pylint: disable=invalid-name
:param iface: A QGIS interface instance.
:type iface: QgsInterface
"""
#
from .amcr_viewer import AmcrViewer
return AmcrViewer(iface)
+19 -83
View File
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
import csv
# -*- coding: utf-8 -*-
import os
import time
import xml.etree.ElementTree as ET # nosec
import csv
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
PLUGIN_DIR = os.path.dirname(__file__)
@@ -65,7 +64,7 @@ def parse_codelist_file(filename, target_dict=None):
try:
# Open the file using standard UTF-8 encoding
with open(path, encoding='utf-8') as f:
with open(path, 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter=';')
# Skip the CSV header row
@@ -91,7 +90,7 @@ def parse_codelist_file(filename, target_dict=None):
except Exception as e:
QgsMessageLog.logMessage(
f"AMČR Codelist Read Error for {filename}: {e}",
"AMČR", Qgis.MessageLevel.Critical)
"AMČR", Qgis.Critical)
return target_dict
@@ -99,26 +98,11 @@ def parse_codelist_file(filename, target_dict=None):
def load_all_data():
"""Loads the codelist during plugin startup."""
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)
return categorized_data
def _facet_name(item):
"""
Returns the value of one facet item from the Digiarchive API.
Digiarchive v4.1.0 (Solr 10, json.nl=arrarr) returns facet items as
["value", count] pairs; older versions returned {"name": "value", ...}
objects. Both shapes are accepted so the plugin works against either.
"""
if isinstance(item, dict):
return item.get("name")
if isinstance(item, (list, tuple)) and item:
return item[0]
return None
def fetch_set(base_url, internal_name, api_set, task=None):
dataset = []
params_amcr = {
@@ -140,9 +124,7 @@ def fetch_set(base_url, internal_name, api_set, task=None):
try:
if "digiarchiv" not in base_url:
response = requests.get(
base_url, params=params_amcr, timeout=30
)
response = requests.get(base_url, params=params_amcr, timeout=30)
response.raise_for_status()
root = ET.fromstring(response.content) # nosec
@@ -211,7 +193,7 @@ def fetch_set(base_url, internal_name, api_set, task=None):
time.sleep(0.5)
else:
break
else:
response = requests.get(base_url, params=params_da, timeout=30)
response.raise_for_status()
@@ -221,61 +203,28 @@ def fetch_set(base_url, internal_name, api_set, task=None):
for r in records:
nazev = _facet_name(r)
if not nazev:
continue
nazev = r["name"]
dataset.append({
'Název': nazev,
'Kód': nazev,
'Kategorie': internal_name
})
break
break
except Exception as e:
# A partial set (e.g. pagination interrupted halfway) would
# silently drop codes – report the whole set as failed instead
# and let the caller keep the previous values
QgsMessageLog.logMessage(
f"Chyba u setu {api_set}: {e}",
"AMČR", Qgis.MessageLevel.Warning)
return []
"AMČR", Qgis.Warning)
break
return dataset
def _read_existing_rows():
"""
Returns the rows of the current heslar.csv grouped by category, so a set
that fails to download can keep its previous values.
"""
rows = {}
if not os.path.exists(OUTPUT_FILE):
return rows
try:
with open(OUTPUT_FILE, encoding='utf-8-sig', newline='') as f:
for row in csv.DictReader(f, delimiter=';'):
cat = (row.get('Kategorie') or '').strip()
if cat:
rows.setdefault(cat, []).append(row)
except Exception as e:
QgsMessageLog.logMessage(
f"Nelze načíst stávající hesláře: {e}",
"AMČR", Qgis.MessageLevel.Warning)
return rows
def download_heslare(task=None, failed=None):
"""
Fetches the codelists from the AMČR API and saves it to a CSV file.
A set that fails or comes back empty keeps its rows from the current
heslar.csv instead of being wiped; its name is appended to ``failed``
(if given) so the caller can warn the user.
"""
def download_heslare(task=None):
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
ensure_codelists_dir()
existing = _read_existing_rows()
all_data = []
total_sets = len(slovnicek)
# index, (interni, api_nazev)
@@ -291,7 +240,7 @@ def download_heslare(task=None, failed=None):
QgsMessageLog.logMessage(
f"Zpracovávám kategorii: {interni}...",
"AMČR", Qgis.MessageLevel.Info)
"AMČR", Qgis.Info)
# Pass the task correctly to the updated fetch function
data = fetch_set(base_url, interni, api_nazev, task=task)
@@ -299,18 +248,6 @@ def download_heslare(task=None, failed=None):
if data is None:
return False # Cancelled mid-download
if not data:
# Never replace a working codelist with nothing – an API change
# would otherwise silently empty the filter in the dialog
old = existing.get(interni, [])
QgsMessageLog.logMessage(
f"Heslář '{interni}' se nepodařilo stáhnout, "
f"ponechávám předchozí hodnoty ({len(old)} položek).",
"AMČR", Qgis.MessageLevel.Warning)
if failed is not None:
failed.append(interni)
data = old
all_data.extend(data)
# Report progress (0-100)
@@ -321,8 +258,7 @@ def download_heslare(task=None, failed=None):
# Save to 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=';',
extrasaction='ignore')
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
writer.writeheader()
writer.writerows(all_data)
+45 -270
View File
@@ -1,67 +1,20 @@
# -*- coding: utf-8 -*-
from qgis.core import (
Qgis,
QgsApplication,
QgsAuthMethodConfig,
QgsMessageLog,
QgsTask,
)
from qgis.gui import QgsDateEdit
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,
)
# -*- coding: utf-8 -*-
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig)
from qgis.utils import iface
from .amcr_codelists import (
AREAL,
DRUH_LOKALITY,
DRUH_NALEZU,
JISTOTA,
KATASTRY,
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"
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
LOKALITA_ZACHOVALOST, PRISTUPNOST,
NALEZ_KATEGORIE, DRUH_NALEZU, SPECIFIKACE,
NALEZOVE_OKOLNOSTI, NALEZCE,
download_heslare, refresh_globals)
# Keep Python references to running tasks. QgsTaskManager only holds the
@@ -72,18 +25,15 @@ _ACTIVE_TASKS = []
class UpdateCodelistsTask(QgsTask):
def __init__(self, description):
super().__init__(description, QgsTask.Flag.CanCancel)
super().__init__(description, QgsTask.CanCancel)
self.success = False
self.exception = None
# Codelists that failed to download and kept their previous values
self.failed_sets = []
def run(self):
"""Runs in a background thread."""
try:
# Call the download function with the task reference
self.success = download_heslare(
task=self, failed=self.failed_sets)
self.success = download_heslare(task=self)
return self.success
except Exception as e:
self.exception = e
@@ -94,27 +44,20 @@ class UpdateCodelistsTask(QgsTask):
if result:
# Safely update the global variables in the main thread
refresh_globals()
if self.failed_sets:
QgsMessageLog.logMessage(
"Hesláře AMČR aktualizovány částečně, beze změny "
f"zůstaly: {', '.join(self.failed_sets)}",
"AMČR", Qgis.MessageLevel.Warning
)
else:
QgsMessageLog.logMessage(
"Hesláře AMČR byly úspěšně aktualizovány.",
"AMČR", Qgis.MessageLevel.Info
)
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.MessageLevel.Warning
"AMČR", Qgis.Warning
)
else:
QgsMessageLog.logMessage(
f"Chyba aktualizace: {self.exception}",
"AMČR", Qgis.MessageLevel.Critical
"AMČR", Qgis.Critical
)
@@ -234,10 +177,6 @@ class AmcrFilterDialog(QDialog):
'nalezce': [],
}
# Date range pickers, filled by setup_date_range():
# (API field, label for messages, 'from' widget, 'to' widget)
self.date_ranges = []
layout = QVBoxLayout()
# Filter by current map canvas extent
@@ -270,6 +209,13 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_katastr)
self.picker_presnost = self.setup_picker(
"PIAN – přesnost",
'pian_presnost',
PIAN_PRESNOST
)
layout.addWidget(self.picker_presnost)
self.picker_pristupnost = self.setup_picker(
"Přístupnost",
'pristupnost',
@@ -279,15 +225,7 @@ class AmcrFilterDialog(QDialog):
# Filters valid for Akce
if self.typ_dat in ["lokalita", "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"]:
if self.typ_dat in ["pas", "akce"]:
self.picker_org = self.setup_picker(
"Organizace",
'organizace',
@@ -312,12 +250,6 @@ class AmcrFilterDialog(QDialog):
)
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
if self.typ_dat == "lokalita":
@@ -354,7 +286,7 @@ class AmcrFilterDialog(QDialog):
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi)
if self.typ_dat == "samostatny_nalez":
if self.typ_dat == "pas":
self.picker_nalez_kategorie = self.setup_picker(
"Kategorie nálezu",
'nalez_kategorie',
@@ -390,14 +322,7 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_nalezce)
# Lokalita has no date field in the index at all, so the block
# 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":
if self.typ_dat != "pas":
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
layout.addWidget(self.picker_areal)
@@ -419,7 +344,7 @@ class AmcrFilterDialog(QDialog):
self.lbl_komponenty_warning.setVisible(False)
layout.addWidget(self.lbl_komponenty_warning)
if self.typ_dat != "samostatny_nalez":
if self.typ_dat != "pas":
self.chk_komponenty.toggled.connect(
self.lbl_komponenty_warning.setVisible
)
@@ -427,21 +352,6 @@ class AmcrFilterDialog(QDialog):
# Pushes everything above to the top
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
buttons = QDialogButtonBox()
@@ -462,9 +372,9 @@ class AmcrFilterDialog(QDialog):
buttons.accepted.connect(self.accept)
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):
"""
@@ -527,94 +437,6 @@ class AmcrFilterDialog(QDialog):
row_widget.setLayout(row_layout)
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):
# Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes
@@ -639,16 +461,6 @@ class AmcrFilterDialog(QDialog):
def on_completed():
_cleanup()
if task.failed_sets:
QMessageBox.warning(
parent_win,
"Hesláře aktualizovány částečně",
"Některé hesláře se nepodařilo stáhnout, "
"ponechány byly jejich předchozí hodnoty:\n"
+ "\n".join(f"• {name}" for name in task.failed_sets)
+ "\n\nPodrobnosti jsou v panelu Zprávy, záložka AMČR."
)
return
QMessageBox.information(
parent_win,
"Hotovo",
@@ -662,7 +474,7 @@ class AmcrFilterDialog(QDialog):
# This will show exactly what went wrong (e.g. PermissionError)
msg = (
"Aktualizace selhala z důvodu chyby:\n"
f"{task.exception!s}"
f"{str(task.exception)}"
)
else:
msg = "Aktualizace byla zrušena uživatelem."
@@ -677,9 +489,7 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_bbox.isChecked() else "false"
def get_komponenty(self):
if self.typ_dat in ["akce", "lokalita"]:
return "true" if self.chk_komponenty.isChecked() else "false"
return "false"
return "true" if self.chk_komponenty.isChecked() else "false"
def get_filters(self):
"""Compiles the user selections from the cache into
@@ -713,7 +523,7 @@ class AmcrFilterDialog(QDialog):
filters['f_vedouci'] = self.selection_cache['vedouci']
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']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
@@ -722,38 +532,7 @@ class AmcrFilterDialog(QDialog):
if self.selection_cache['jistota']:
filters['f_jistota'] = self.selection_cache['jistota']
if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = (
self.selection_cache['lokalita_zachovalost']
)
# 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}"
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
return filters
@@ -775,8 +554,7 @@ class LoginDialog(QDialog):
- 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.
- loadAuthenticationConfig() with full=False loads only metadata
(name, method,
- loadAuthenticationConfig() with full=False loads only metadata (name, method,
id) but NOT the config() values like username/password. Use full=True to
access those.
"""
@@ -1007,7 +785,7 @@ class LoginDialog(QDialog):
# 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, _ = (
ok_load, existing_cfg = (
self._load_config(existing_id, full=False)
if existing_id
else (False, None)
@@ -1036,9 +814,7 @@ class LoginDialog(QDialog):
settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id:
QgsApplication.authManager().removeAuthenticationConfig(
existing_id
)
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
settings.remove(self.SETTINGS_KEY)
QMessageBox.information(
self,
@@ -1073,5 +849,4 @@ class LoginDialog(QDialog):
if not ok:
return "", ""
return (cfg.config("username", ""),
cfg.config("password", "")) # nosec B106
return cfg.config("username", ""), 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 -*-
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.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_dialog import AmcrFilterDialog, LoginDialog
import os.path
class AmcrViewer:
@@ -30,7 +29,7 @@ class AmcrViewer:
locale_path = os.path.join(
self.plugin_dir,
'i18n',
f'AmcrViewer_{locale}.qm'
'AmcrViewer_{}.qm'.format(locale)
)
# Install the translator if a translation file
@@ -42,7 +41,7 @@ class AmcrViewer:
# Initialize internal state
self.actions = []
self.menu = self.tr('&AMČR Viewer')
self.menu = self.tr(u'&AMČR Viewer')
self.first_start = None
def tr(self, message):
@@ -91,7 +90,7 @@ class AmcrViewer:
"""
# Define paths for action-specific icons
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_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png')
@@ -103,7 +102,7 @@ class AmcrViewer:
# custom dropdown menu
self.action_download_akce = self.add_action(
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'),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -113,9 +112,8 @@ class AmcrViewer:
self.action_download_pas = self.add_action(
icon_path=icon_pas_path,
text=self.tr('Stáhnout data samostatných nálezů | AMČR Viewer'),
callback=lambda checked=False: self.run_download(
'samostatny_nalez'),
text=self.tr(u'Stáhnout data samostatných nálezů | AMČR Viewer'),
callback=lambda checked=False: self.run_download('pas'),
parent=self.iface.mainWindow(),
add_to_menu=False,
add_to_toolbar=False
@@ -124,7 +122,7 @@ class AmcrViewer:
self.action_download_lokality = self.add_action(
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'),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -133,8 +131,8 @@ class AmcrViewer:
self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action(
icon_path=icon_pas_path,
text=self.tr('Přihlásit se | AMČR Viewer'),
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,
@@ -144,7 +142,7 @@ class AmcrViewer:
self.action_amcr_help = self.add_action(
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(),
parent=self.iface.mainWindow(),
add_to_menu=False,
File diff suppressed because it is too large. Load diff
+4 -19
View File
@@ -8,7 +8,7 @@ name=AMČR Viewer
qgisMinimumVersion=3.44.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data.
version=2.1.4
version=2.0.2
author=David Spáčil
email=spacil@arub.cz
@@ -23,24 +23,9 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
hasProcessingProvider=no
# Uncomment the following line and add your 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
v2.1.4 (2026-10-01)
* Removed unused generated resources.py and the bundled flake8 config, so the plugin passes the plugins.qgis.org scan without custom configuration
v2.1.3 (2026-10-01)
* Fixed empty person codelists (excavation leaders, finders) after updating codelists against Digiarchive v4.1.0
* A codelist that fails to download keeps its previous values and the user is warned
v2.1.2 (2026-09-01)
* Qt6 compatibility
* 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
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.
Version 2.0.0 changes the plugin behavior dramatically. It is advised to check the changelog below.
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.0.0
v2.0.2 (2026-06-13)
* Plugin-wide fixes and optimalizations (details https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/pull/49)
v2.0.1 (2026-06-10)
+128
View File
@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.13)
#
# WARNING! All changes made in this file will be lost!
from qgis.PyQt import QtCore
qt_resource_data = b"\
\x00\x00\x04\x0a\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x17\x00\x00\x00\x18\x08\x06\x00\x00\x00\x11\x7c\x66\x75\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\
\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\
\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd9\x02\x15\
\x16\x11\x2c\x9d\x48\x83\xbb\x00\x00\x03\x8a\x49\x44\x41\x54\x48\
\xc7\xad\x95\x4b\x68\x5c\x55\x18\xc7\x7f\xe7\xdc\x7b\x67\xe6\xce\
\x4c\x66\x26\x49\xd3\x24\x26\xa6\xc6\xf8\x40\x21\xa5\x04\xb3\x28\
\xda\x98\x20\xa5\x0b\xad\x55\xa8\x2b\xc5\x50\x1f\xa0\x6e\x34\x2b\
\x45\x30\x14\x02\xba\x52\x69\x15\x17\x66\x63\x45\x97\x95\xa0\xad\
\x0b\xfb\xc0\x06\x25\xb6\x71\x61\x12\x41\x50\xdb\x2a\x21\xd1\xe2\
\x24\xf3\x9e\xc9\xcc\xbd\xe7\x1c\x17\x35\x43\x1e\x33\x21\xb6\xfd\
\x56\x87\xf3\x9d\xfb\xfb\x1e\xf7\xff\x9d\x23\x8c\x31\x43\x95\xf4\
\x85\x1e\x3f\x3b\x35\xac\xfd\xcc\x43\xdc\xa4\x49\x3b\xfe\x9d\x1d\
\xdb\x7b\x22\x90\x78\xf8\xb2\x28\xa7\xbe\x7d\xc1\x4b\x9d\x79\xdf\
\x18\x15\xe5\x16\x99\x10\x56\xde\x69\xdc\x3f\x22\xfd\xec\xd4\xf0\
\xad\x04\x03\x18\xa3\xa2\x7e\x76\x6a\x58\xde\x68\x2b\xb4\x36\xf8\
\xbe\xc6\x18\x53\xdb\xef\xe7\xfa\xec\xed\x67\x63\x10\x42\x00\xf0\
\xfb\xd5\x65\x2a\x15\x45\xc7\x6d\x0d\x00\xc4\xa2\xc1\xaa\x6f\x0d\
\x3e\x6c\xab\xc2\x1c\x56\xa4\x77\x4b\xb0\xf2\x35\x15\x5f\x21\x85\
\xe0\xc8\x6b\x5f\x92\x2d\x37\x33\x39\xf9\x03\x27\x8e\x1f\xa2\xf7\
\xbe\x9d\x04\x1c\x0b\x37\xe4\xac\xff\xa6\x30\x87\xbd\xba\x00\x6a\
\x06\x79\xe5\xf5\xaf\x89\xd9\x92\xc5\xcc\x0a\xd9\x7c\x19\xcf\xe9\
\xe2\xe4\xa9\x2f\x78\x7c\xff\x01\x72\x85\x0a\x2b\x65\x1f\xa5\x4c\
\xb5\xb2\x55\x16\x80\xbd\x31\xda\xda\x20\x1f\x7d\x3e\xcd\xc2\xfd\
\x59\xa6\x93\x39\x92\xd1\x22\xea\x9b\x16\xce\x9d\x3f\xce\xe0\x83\
\x03\x24\x82\x59\x3a\xdb\x7b\x88\xc7\x82\x68\x63\x58\xc9\xcc\x62\
\x8c\x21\x18\xb0\x6a\xc3\x37\x06\x49\x16\xff\x24\x6b\xa5\x49\xbb\
\x25\xbc\xa2\xa6\x21\xbb\x40\x7f\xdf\x00\x83\xbd\x01\x8e\x3c\xd5\
\x45\xd7\x8e\x6b\x9c\x9c\x98\x25\x1a\xb6\xe8\xbe\x3d\xc2\xdd\x77\
\x44\x48\xc4\x1c\x22\xe1\xeb\x58\x59\xaf\xcf\xd3\x33\x29\x2e\x34\
\x2d\x91\x93\x3e\xbe\x34\x78\x01\xc5\xe2\x61\xc5\xae\x72\x8e\x70\
\xc8\xc2\x0d\x5a\xbc\xf5\xee\x2f\x9c\xfa\x3e\x86\x69\x7a\x8e\xcf\
\x26\xe6\xf9\x63\xa1\x44\xa1\xa4\xd0\xda\x6c\x0d\x2f\x15\x7c\xb4\
\x67\x28\x59\x0a\xcf\xd6\x54\xe2\x06\x13\x87\x2b\x6f\x68\xa6\x27\
\xaf\x31\x32\x36\xc7\xb2\x7f\x17\xef\x7d\x7c\x8c\x33\x67\xcf\x12\
\x70\x24\x4a\x69\xd6\x6a\x46\xd6\xd3\x70\x72\xa9\x82\x67\x34\x45\
\xad\x28\xdb\x1a\x15\x34\x98\xff\x46\xed\xef\x37\x0d\x99\xbf\x4a\
\x3c\x30\x38\xc0\xc8\x4b\xaf\x92\x5a\x9c\xe2\xe0\x23\x6d\x74\xb4\
\xba\x84\x5d\x0b\x29\x45\x7d\xb8\x94\x82\x96\xb6\x10\xf3\xc5\x12\
\x2a\xef\x53\x11\x1a\x63\xad\x3f\x93\x19\x85\xf1\xb1\x77\x58\x5a\
\xf8\x99\x97\x9f\xe9\xa6\x75\x47\x90\xc6\xb8\x43\xd8\xb5\xb6\xce\
\xfc\xfa\xfd\x00\xfb\x3e\xf4\xc8\x05\x35\xba\x5e\xeb\x46\x21\xf9\
\xcf\x0a\xa9\x8c\x87\xe3\x48\xdc\x90\xb5\x6e\x98\x6a\xaa\x65\xf2\
\x52\x92\x43\x2f\x5e\xc2\x8c\x02\x1a\x10\xf5\x07\xac\xc3\x75\x70\
\x83\x92\x80\xb3\xf9\xd0\x26\xf8\x8f\xb3\x29\xc6\x3e\xb8\x8c\x19\
\x35\x75\x6b\x7b\x7e\x3c\xca\x45\x0c\x7e\x49\x31\xf4\x58\x3b\xf7\
\xf6\x34\x90\x88\x39\x04\x1c\x59\x1f\xfe\xdb\xd5\x3c\x5f\x9d\x4b\
\x32\xfd\x44\xb2\xba\xd7\xfa\xb6\x60\xcf\xde\x16\xdc\x90\x45\x4c\
\x4a\x2a\x9e\x62\xfe\x4e\xc5\xc8\xc1\x4e\xda\x76\x86\xe8\xe9\x0a\
\xe3\xd8\x92\x58\xd4\xc6\xb2\x44\x6d\x78\x2a\x53\xe1\xca\x7c\x99\
\x63\x5d\xbf\x56\x9d\xbd\x9f\x44\x18\x7a\xba\x95\x27\x0f\xb4\xd3\
\xdc\x18\xc0\xf3\x0d\x52\x40\xd8\xb5\xb0\xa4\x20\x14\xb2\x70\x6c\
\x81\x63\xcb\xaa\x42\xd6\xfd\xb7\xf4\xec\xa3\x06\xa0\x50\x52\xd8\
\x4e\x1b\x7e\x4a\xd3\x31\xf9\x29\xcf\xfe\xd4\x49\x7f\x5f\x13\xfb\
\xfa\x9b\x71\x43\x92\x58\xd4\x21\x18\x90\xac\xde\xb0\x42\x50\x13\
\x58\x33\xf3\x88\x6b\xa1\xfd\x65\x96\xf2\x79\xc6\x43\x7b\xd8\x75\
\x38\xcc\x3d\xdd\xd1\xaa\xcf\x71\xe4\xff\x7f\x91\x56\x33\xaf\xea\
\x37\xe7\xa1\x94\x21\x16\xb5\xd1\x06\x2c\x29\x36\xf5\x72\x9b\x96\
\x95\xc0\xc4\xda\x9d\x78\x83\x43\x53\x22\x80\x65\x09\x1c\xfb\x86\
\xc1\x00\xe7\x25\x70\x14\x48\x6f\x1e\x22\x51\xe3\x75\xd9\xb6\xa5\
\x81\xa3\x32\xb1\xfb\xf4\x0c\x30\xb8\xb1\x82\x9b\xb0\x09\x60\x30\
\xb1\xfb\xf4\xcc\xbf\xa0\xe9\x6e\xae\x5a\xdf\x4b\x81\x00\x00\x00\
\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
"
qt_resource_name = b"\
\x00\x07\
\x07\x3b\xe0\xb3\
\x00\x70\
\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\
\x00\x0b\
\x06\x1f\xb8\xc2\
\x00\x61\
\x00\x6d\x00\x63\x00\x72\x00\x5f\x00\x76\x00\x69\x00\x65\x00\x77\x00\x65\x00\x72\
\x00\x08\
\x0a\x61\x5a\xa7\
\x00\x69\
\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
"
qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\
\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
"
qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9c\x23\xfd\x16\x70\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
if qt_version < [5, 8, 0]:
rcc_version = 1
qt_resource_struct = qt_resource_struct_v1
else:
rcc_version = 2
qt_resource_struct = qt_resource_struct_v2
def qInitResources():
QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
def qCleanupResources():
QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
qInitResources()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

-43
View File
@@ -1,43 +0,0 @@
# Konfigurace lintů pro tento repozitář.
#
# Plugin se nedistribuuje jako Python balíček (do QGISu jde ZIP složky
# amcr_viewer/), takže tenhle soubor nic nebalí ani neinstaluje – slouží
# jen k tomu, aby ruff choval stejně v CI, lokálně i za rok. Bez explicitní
# konfigurace se výchozí sada pravidel mezi verzemi ruffu mění.
#
# Flake8 záměrně žádnou konfiguraci nemá a běží s výchozími pravidly – stejně
# jako scanner na plugins.qgis.org. Config soubor v balíčku by plugin
# označil jako „Validated (configured)“.
[tool.ruff]
line-length = 79
# QGIS 3.44 běží na Pythonu 3.9 a novějším
target-version = "py39"
[tool.ruff.lint]
select = [
"E", # pycodestyle – chyby
"W", # pycodestyle – varování
"F", # pyflakes
"I", # pořadí importů
"UP", # zastaralé konstrukce
"B", # bugbear
"C4", # comprehensions
"SIM", # zjednodušení
"RET", # návratové hodnoty
"BLE", # holé except
]
ignore = [
# Hlavička "# -*- coding: utf-8 -*-" je konvence šablony Plugin
# Builderu a drží se v celém projektu jednotně.
"UP009",
# "except Exception" je v pluginu záměr: výjimka nesmí propadnout do
# QGISu, chyba se uživateli ukáže v liště zpráv.
"BLE001",
# Obě dotčená místa mají ke každé větvi vysvětlující komentář,
# sloučením do jednoho výrazu by se čitelnost zhoršila.
"SIM103",
# contextlib.suppress() by kvůli jednomu místu přidal import a odsunul
# komentář, který vysvětluje, proč tam ta výjimka je.
"SIM105",
]
+7 -23
View File
@@ -9,7 +9,7 @@
id="svg1"
xml:space="preserve"
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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@@ -24,12 +24,12 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="22.627417"
inkscape:cx="0.57452426"
inkscape:cy="15.556349"
inkscape:cx="6.8059028"
inkscape:cy="14.606174"
inkscape:window-width="1920"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
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 "
inkscape:export-filename="path32.png"
inkscape:export-xdpi="3000"
inkscape:export-ydpi="3000" /><rect
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>
inkscape:export-ydpi="3000" /></g></svg>

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

-83
View File
@@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
"""
Repository hygiene rules that need no QGIS and therefore run first.
Each rule guards a mistake that has already happened here at least once,
or one the plugins.qgis.org file analysis reports:
* a UTF-8 BOM makes the official pyqgis4-checker skip the file entirely,
so a broken file looks clean – it is silent, which is what makes it bad
* a direct PyQt5/PyQt6 import breaks the other Qt version
* an executable or hidden file in the package is reported on upload
Run it from the repository root:
python3 tests/check_sources.py
"""
import os
import re
import stat
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BALICEK = os.path.join(ROOT, "amcr_viewer")
# Extensions that have no business inside a plugin package
PODEZRELE = {".exe", ".dll", ".so", ".dylib", ".sh", ".bat", ".cmd",
".pyc", ".pyd", ".jar", ".bin"}
PRIMY_IMPORT = re.compile(r"^\s*(?:from|import)\s+PyQt[56]\b", re.MULTILINE)
nalezy = []
def zdrojaky():
for adresar, _, soubory in os.walk(BALICEK):
for soubor in sorted(soubory):
if soubor.endswith(".py"):
yield os.path.join(adresar, soubor)
def vsechny_soubory():
for adresar, _, soubory in os.walk(BALICEK):
for soubor in sorted(soubory):
yield os.path.join(adresar, soubor)
def zkratka(cesta):
return os.path.relpath(cesta, ROOT)
for cesta in zdrojaky():
with open(cesta, "rb") as f:
zacatek = f.read(3)
if zacatek == b"\xef\xbb\xbf":
nalezy.append(f"{zkratka(cesta)}: UTF-8 BOM na začátku souboru")
with open(cesta, encoding="utf-8-sig") as f:
text = f.read()
for shoda in PRIMY_IMPORT.finditer(text):
radek = text[:shoda.start()].count("\n") + 1
nalezy.append(f"{zkratka(cesta)}:{radek}: přímý import z PyQt5/PyQt6, "
f"použij shim qgis.PyQt")
for cesta in vsechny_soubory():
jmeno = os.path.basename(cesta)
rezim = os.stat(cesta).st_mode
if rezim & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
nalezy.append(f"{zkratka(cesta)}: spustitelná práva "
f"({stat.filemode(rezim)})")
# No exceptions: scanner config files (.flake8, .bandit,
# .secrets.baseline) would mark the upload "Validated (configured)"
if jmeno.startswith("."):
nalezy.append(f"{zkratka(cesta)}: skrytý soubor v balíčku pluginu")
if os.path.splitext(jmeno)[1].lower() in PODEZRELE:
nalezy.append(f"{zkratka(cesta)}: podezřelý typ souboru")
if nalezy:
print("Nálezy:")
for nalez in nalezy:
print(f" {nalez}")
sys.exit(1)
print("Kontrola zdrojáků: bez nálezů")
-130
View File
@@ -1,130 +0,0 @@
# -*- coding: utf-8 -*-
"""
Smoke test: loads the plugin inside a real QGIS and exercises the parts
that differ between Qt5 and Qt6.
It is deliberately offline – no request ever leaves the machine, so the
test says nothing about the AMCR API, only about the plugin loading and
its widgets being constructible.
Run it from the repository root:
python3 tests/smoke_test.py
QGIS must be importable (inside the qgis/qgis Docker image it already is).
The exit code is 0 when everything passed, 1 otherwise.
"""
import os
import sys
import traceback
# Offscreen, otherwise the dialogs need an X server
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
selhani = []
def zkouska(nazev, funkce):
"""Runs one check and keeps going even when it raises."""
try:
detail = funkce()
except Exception:
selhani.append(nazev)
print(f" FAIL {nazev}")
print(traceback.format_exc().rstrip())
else:
print(f" OK {nazev}" + (f" – {detail}" if detail else ""))
from qgis.core import ( # noqa: E402
Qgis,
QgsApplication,
QgsTask,
QgsWkbTypes,
)
from qgis.PyQt import QtCore # noqa: E402
from qgis.PyQt.QtCore import QDate # noqa: E402
print(f"QGIS {Qgis.QGIS_VERSION.split('-')[0]} | Qt {QtCore.QT_VERSION_STR} "
f"| PyQt {QtCore.PYQT_VERSION_STR}")
QgsApplication.setPrefixPath(os.environ.get("QGIS_PREFIX_PATH", "/usr"), True)
qgs = QgsApplication([], True)
qgs.initQgis()
import amcr_viewer.amcr_codelists # noqa: E402,F401
import amcr_viewer.amcr_dialog as dialog # noqa: E402
import amcr_viewer.amcr_tools # noqa: E402,F401
import amcr_viewer.amcr_viewer # noqa: E402,F401
print(" OK import všech modulů pluginu")
def enumy():
"""
The scoped enum forms must exist. Unscoped aliases still resolve in
QGIS 4.2, so a plain import proves nothing – these are read explicitly.
"""
return (f"QgsTask.Flag.CanCancel={int(QgsTask.Flag.CanCancel)}, "
f"PointGeometry={int(QgsWkbTypes.GeometryType.PointGeometry)}, "
f"MessageLevel.Info={int(Qgis.MessageLevel.Info)}")
def uloha():
ukol = dialog.UpdateCodelistsTask("smoke")
assert ukol.canCancel() is True
return "canCancel=True"
def dialogy():
# A modal warning would block the offscreen run forever
dialog.QMessageBox.warning = staticmethod(lambda *a, **k: None)
popis = []
for typ in ("akce", "lokalita", "samostatny_nalez"):
okno = dialog.AmcrFilterDialog(typ)
okno.show()
QgsApplication.processEvents()
popis.append(f"{typ}: {len(okno.date_ranges)} rozmezí")
okno.close()
return ", ".join(popis)
def filtr_datumu():
"""
A half-filled range must be completed with the sentinel. The API
rejects a one-sided range, so this is the part worth guarding.
The expected value is written out on purpose – comparing against
dialog.DATE_OPEN_TO would only prove the module agrees with itself.
"""
okno = dialog.AmcrFilterDialog("samostatny_nalez")
pole, _, od, _do = okno.date_ranges[0]
od.setDate(QDate(2016, 1, 1))
hodnota = okno.get_filters()[pole]
assert hodnota == "2016-01-01,9999-12-31", hodnota
# A range left completely empty must add no filter at all
prazdne = dialog.AmcrFilterDialog("samostatny_nalez")
pole_prazdne = prazdne.date_ranges[0][0]
assert pole_prazdne not in prazdne.get_filters()
prazdne.close()
okno.close()
return hodnota
zkouska("scoped enumy", enumy)
zkouska("UpdateCodelistsTask", uloha)
zkouska("filtrační dialogy", dialogy)
zkouska("filtr podle data", filtr_datumu)
qgs.exitQgis()
if selhani:
print(f"\nNEPROŠLO: {', '.join(selhani)}")
sys.exit(1)
print("\nVše prošlo")