mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-10-11 13:27:33 +02:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b783cd13b | ||
|
|
06036f2af0 | ||
|
|
8fa81394f2 | ||
|
|
1d5220ee61 | ||
|
|
70aba60d12 | ||
|
|
3591ac4c53 | ||
|
|
f4b1c39e6c | ||
|
|
bcb0bc2c86 | ||
|
|
39dac12bad | ||
|
|
b708930ca2 | ||
|
|
3f7839c818 | ||
|
|
9aba28317a | ||
|
|
048ffe4e2a | ||
|
|
d398d2cd1b | ||
|
|
2218719f98 | ||
|
|
6300919c36 | ||
|
|
7cffcdb235 | ||
|
|
0af4a0cd92 | ||
|
|
9b25863031 | ||
|
|
9ec71c7ed3 | ||
|
|
4ed99d73d9 | ||
|
|
4699dd9c95 | ||
|
|
7ea2a99ada | ||
|
|
6772a99ead | ||
|
|
9e8863b879 | ||
|
|
0eb8008e27 | ||
|
|
bc799452a3 | ||
|
|
ee558aa718 | ||
|
|
eebd7668a5 | ||
|
|
fd11bee274 | ||
|
|
93ed0ca810 | ||
|
|
64ec1ea7fd | ||
|
|
27e5fe02ac | ||
|
|
46c09c4a09 | ||
|
|
444d1c4826 | ||
|
|
d9f5d2ae6e | ||
|
|
d417a78b85 | ||
|
|
ca827321d8 | ||
|
|
b2001c625c | ||
|
|
88018aa432 | ||
|
|
45b6ab09b2 | ||
|
|
89e596802f | ||
|
|
4ea679ec9e | ||
|
|
830537f1a4 | ||
|
|
785b83c9c5 | ||
|
|
493696c67b | ||
|
|
b313fc6db0 | ||
|
|
9a935261e6 | ||
|
|
a4e30bf334 | ||
|
|
56389e27d7 | ||
|
|
c8d42e2459 |
No files matched your search
@@ -0,0 +1,35 @@
|
|||||||
|
<!--
|
||||||
|
Děkujeme za příspěvek! Vyplň prosím sekce níže.
|
||||||
|
Nepotřebné body můžeš smazat. Komentáře (<!-- ... -->) se v PR nezobrazují.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Souhrn
|
||||||
|
|
||||||
|
<!-- Stručně co a proč. 1–3 věty. -->
|
||||||
|
|
||||||
|
## Změny
|
||||||
|
|
||||||
|
<!-- Co se konkrétně mění, ideálně po souborech / oblastech. -->
|
||||||
|
-
|
||||||
|
|
||||||
|
## Testování
|
||||||
|
|
||||||
|
<!-- Jak ověřit, že to funguje (kroky v QGIS, scénáře). -->
|
||||||
|
-
|
||||||
|
|
||||||
|
## Zapojení AI
|
||||||
|
|
||||||
|
<!-- Byla u změny použita AI? Jak? Např. "text navržen AI, ručně zkontrolováno
|
||||||
|
a upraveno". Smaž, pokud AI použita nebyla. -->
|
||||||
|
-
|
||||||
|
|
||||||
|
## 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` 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>`)
|
||||||
|
|
||||||
|
## Související issue
|
||||||
|
|
||||||
|
<!-- Např. "Closes #123". Smaž, pokud se neváže k issue. -->
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
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 – konfigurace
|
||||||
|
# v amcr_viewer/.flake8 je stejná pro obě místa.
|
||||||
|
- name: Flake8
|
||||||
|
run: flake8 --config amcr_viewer/.flake8 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*"
|
||||||
|
|
||||||
|
# Kontrola obsahu ZIPu. Config soubory pro scanner musí být uvnitř
|
||||||
|
# vedle metadata.txt, jinak je plugins.qgis.org nenajde – a některé
|
||||||
|
# nástroje skryté soubory tiše vynechávají.
|
||||||
|
- name: Verify archive contents
|
||||||
|
run: |
|
||||||
|
unzip -l amcr_viewer.zip
|
||||||
|
for soubor in amcr_viewer/metadata.txt amcr_viewer/__init__.py \
|
||||||
|
amcr_viewer/.flake8; 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
|
||||||
@@ -1,17 +1,28 @@
|
|||||||
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:
|
||||||
release:
|
push:
|
||||||
types: [published]
|
tags:
|
||||||
|
- '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 repozitáře
|
# 1. Stáhne kód z tagu, který běh spustil
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
# 2. Vytvoří ZIP (předpokládá, že kód je ve složce 'amcr_viewer')
|
# 2. Vytvoří ZIP (předpokládá, že kód je ve složce 'amcr_viewer')
|
||||||
- name: Zip Plugin
|
- name: Zip Plugin
|
||||||
@@ -20,11 +31,14 @@ 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. Nahraje ZIP k Releasu
|
# 3. Založí koncept releasu i s přílohou
|
||||||
- name: Upload Release Asset
|
# Tagy s pomlčkou (v2.0.0-alpha.1) se označí jako pre-release.
|
||||||
uses: softprops/action-gh-release@v1
|
- name: Create draft release with asset
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||||
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 }}
|
||||||
@@ -211,3 +211,4 @@ __marimo__/
|
|||||||
README_files/
|
README_files/
|
||||||
README.html
|
README.html
|
||||||
amcr_viewer.zip
|
amcr_viewer.zip
|
||||||
|
pyrefly.toml
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Pokyny pro AI agenty i lidské přispěvatele pracující v tomto repozitáři.
|
||||||
|
Tento soubor je jediný zdroj pravdy; `CLAUDE.md` na něj pouze odkazuje.
|
||||||
|
|
||||||
|
## O projektu
|
||||||
|
|
||||||
|
**AMČR Viewer** je plugin do QGIS pro stahování a vizualizaci dat z Digitálního
|
||||||
|
archivu Archeologické mapy ČR (AMČR / AIS CR) – akce (*Fieldwork events*),
|
||||||
|
lokality (*Sites*) a jejich komponenty. Podporuje anonymní i přihlášený přístup
|
||||||
|
přes AMČR účet.
|
||||||
|
|
||||||
|
Zdroj dat: https://digiarchiv.aiscr.cz/ · Nápověda: https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
|
||||||
|
|
||||||
|
## Zařazení v ekosystému AIS CR
|
||||||
|
|
||||||
|
Tento repozitář je jedním ze **sourozeneckých repozitářů** ekosystému AIS CR.
|
||||||
|
Centrální governance a AI konfigurace spravuje hub **`aiscr-management`**; konvence
|
||||||
|
v tomto souboru jsou s tímto vzorem sladěné a zjednodušené pro potřeby jednoho
|
||||||
|
QGIS pluginu. Těžkou mašinerii hubu (složka `.agents/`, OpenSpec, sync skripty,
|
||||||
|
multi-assistant generování) tento repozitář **záměrně nepřebírá**. Při širších
|
||||||
|
otázkách governance má přednost vzor z `aiscr-management`.
|
||||||
|
|
||||||
|
## Struktura repozitáře
|
||||||
|
|
||||||
|
```
|
||||||
|
amcr_viewer/ # vlastní kód pluginu (toto se balí do releasu)
|
||||||
|
amcr_viewer.py # vstupní bod pluginu, integrace do QGIS
|
||||||
|
amcr_dialog.py # dialogy a UI (filtry, přihlášení)
|
||||||
|
amcr_tools.py # stahování dat z API, sestavení vrstev a atributů
|
||||||
|
amcr_codelists.py # hesláře / číselníky (codelists)
|
||||||
|
codelists/heslar.csv # lokální kopie číselníků
|
||||||
|
metadata.txt # metadata pluginu + verze + changelog
|
||||||
|
i18n/ # překlady (.ts)
|
||||||
|
*.png # ikony
|
||||||
|
.github/workflows/ # CI – release pluginu
|
||||||
|
README.md # uživatelská dokumentace (anglicky)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konvence
|
||||||
|
|
||||||
|
### Jazyk
|
||||||
|
- **Kód a identifikátory:** anglicky. Atributová pole vrstev musí být ASCII
|
||||||
|
kompatibilní (bez diakritiky) – viz historie změn v `metadata.txt`.
|
||||||
|
- **README a uživatelská dokumentace:** anglicky.
|
||||||
|
- **Commity, PR a komentáře v issue:** česky.
|
||||||
|
|
||||||
|
### Commity
|
||||||
|
- Styl odpovídá historii: česky, věcně, popisně; jeden commit = jedna logická
|
||||||
|
změna.
|
||||||
|
- První řádek stručně a výstižně (ideálně v imperativu); podrobnosti do těla.
|
||||||
|
- Pokud je commit připraven s pomocí AI, uveď to v těle commitu nebo v popisu PR.
|
||||||
|
|
||||||
|
### Větve
|
||||||
|
Konvence názvů je sladěná s hubem `aiscr-management`:
|
||||||
|
|
||||||
|
- **lidé:** `feat/<téma>` (nová funkce), `fix/<téma>` (oprava), `docs/<téma>`
|
||||||
|
(dokumentace), `chore/<téma>` (údržba).
|
||||||
|
- **AI agenti:** `agents/<jméno-agenta>/<téma>` (např. `agents/claude/oprava-pian`).
|
||||||
|
|
||||||
|
Další pravidla:
|
||||||
|
|
||||||
|
- Cílová větev pro nový vývoj je aktuální `version/v2.x.y` (ne přímo do
|
||||||
|
výchozí větve bez PR).
|
||||||
|
- **Nikdy** nepushuj přímo do chráněných větví; vždy přes Pull Request.
|
||||||
|
- Standardizační / nefunkční změny drž v samostatné větvi, ať se nemíchají do
|
||||||
|
feature PR.
|
||||||
|
|
||||||
|
### Pravidla pro AI agenty (git)
|
||||||
|
- AI ve výchozím stavu zůstává u **lokální práce na aktuální větvi**.
|
||||||
|
- Bez **výslovného pokynu** uživatele AI nestageuje (`git add`), necommituje,
|
||||||
|
nepushuje ani samo nepřepíná/nezakládá větev pro vzdálené doručení.
|
||||||
|
- Při výslovném požadavku na push/PR použij větev `agents/<jméno-agenta>/<téma>`;
|
||||||
|
pokud aktuální větev tomuto vzoru neodpovídá, vyžádej si nejdřív potvrzení.
|
||||||
|
- Vytvoření větve, stage, commit, push ani draft PR AI běžně nenabízí; zmiňuj je
|
||||||
|
jen tehdy, když jsou pro splnění úkolu opravdu nutné.
|
||||||
|
|
||||||
|
### QGIS specifika
|
||||||
|
- Minimální podporovaná verze QGIS je **3.44** (`qgisMinimumVersion` v
|
||||||
|
`metadata.txt`); kód nesmí spoléhat na novější API.
|
||||||
|
- Vrstvy a atributy se sestavují přes `QgsField` / QGIS API v `amcr_tools.py`.
|
||||||
|
Při přidání atributu je potřeba doplnit ho konzistentně na všech místech:
|
||||||
|
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).
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Pull requesty
|
||||||
|
|
||||||
|
- Používej PR šablonu (`.github/pull_request_template.md`): Souhrn / Změny /
|
||||||
|
Testování / Kontrolní seznam.
|
||||||
|
- PR musí mířit do správné `version/v2.x.y` větve.
|
||||||
|
- Před požádáním o review projdi kontrolní seznam v šabloně (zejména bump verze
|
||||||
|
v `metadata.txt`, pokud měníš chování).
|
||||||
|
- V popisu PR uveď **podíl AI** (např. „text navržen AI, ručně zkontrolováno")
|
||||||
|
a odkaz na související issue, pokud existuje.
|
||||||
|
|
||||||
|
## Bezpečnost a soukromí
|
||||||
|
|
||||||
|
- Do promptů, příkladů ani commitů **nevkládej** ostrá produkční data, plné
|
||||||
|
log dumpy ani reálné osobní údaje (PII).
|
||||||
|
- **Rediguj** secrets, tokeny, API klíče a hesla z čehokoli, co posíláš AI;
|
||||||
|
nikdy je necommituj do repozitáře (ani přihlašovací údaje k AMČR účtu).
|
||||||
|
- Pro interní infrastrukturu (URL, hostname, prostředí) používej placeholdery,
|
||||||
|
pokud konkrétní hodnota není nutná a povolená.
|
||||||
|
|
||||||
|
## Lokální ověření
|
||||||
|
|
||||||
|
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** | 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 --config amcr_viewer/.flake8 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.
|
||||||
|
- **Konfigurace lintů je rozdělená schválně.** `amcr_viewer/.flake8` leží
|
||||||
|
vedle `metadata.txt`, protože scanner na plugins.qgis.org hledá config
|
||||||
|
soubory jen v kořeni balíčku uvnitř ZIPu; díky tomu platí stejná pravidla
|
||||||
|
v CI, lokálně i při uploadu. Konfigurace ruffu je naopak 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.
|
||||||
+6
-7
@@ -20,11 +20,10 @@ identifiers:
|
|||||||
value: 10.5281/zenodo.18609813
|
value: 10.5281/zenodo.18609813
|
||||||
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
|
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
|
||||||
abstract: >-
|
abstract: >-
|
||||||
This QGIS plugin is intended for downloading the data
|
This plugin is intended for downloading the data
|
||||||
(Fieldwork events data only, at the time) from the
|
(Fieldwork events, Sites, and their Components) from
|
||||||
Digiarchive of the Archaeological Map of the Czech
|
the Digital archive of the Archaeological Map of the
|
||||||
Republic (AMCR). As of now, only publicly accessible data
|
Czech Republic (https://digiarchiv.aiscr.cz/).
|
||||||
can be downloaded.
|
|
||||||
license: GPL-3.0
|
license: GPL-3.0
|
||||||
version: '1.0.1'
|
version: '2.1.2'
|
||||||
date-released: '2026-02-11'
|
date-released: '2026-09-01'
|
||||||
@@ -1,170 +1,433 @@
|
|||||||
# AMCR Viewer: QGIS Plugin Documentation
|
# AMČR Viewer — QGIS plugin
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[](https://qgis.org/)
|
||||||
|
[](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/actions/workflows/code_quality.yml)
|
||||||
|
[](https://doi.org/10.5281/zenodo.18609813)
|
||||||
|
|
||||||
**Platform:** QGIS 3.4.0–4.99.0
|
**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.
|
||||||
|
|
||||||
**Module Type:** Data Acquisition & Visualization
|
| | |
|
||||||
|
| --- | --- |
|
||||||
**Source Data:** Archaeological Map of the Czech Republic (AIS CR)
|
| **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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Overview
|
## 1. What it can download
|
||||||
|
|
||||||
**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 be accompanied by a *Components* layer with additional information. **Only publicly accessible data are supported at the time** (accessibility = anonymous).
|
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. |
|
||||||
|
|
||||||
### Key Features
|
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.
|
||||||
|
|
||||||
* **Spatial Querying:** Option to filter records based on the current map canvas extent (Bounding Box).
|
### Key features
|
||||||
* **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.
|
|
||||||
|
|
||||||
## 2. Installation Guide
|
* **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.
|
||||||
|
|
||||||
**Install the plugin from QGIS plugin repository.**
|
---
|
||||||
|
|
||||||
**OR**
|
## 2. Installation
|
||||||
|
|
||||||
*1. Obtain the [plugin distribution package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases) (ZIP archive containing the `amcr_viewer` directory).*
|
### From the QGIS plugin repository (recommended)
|
||||||
*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 download button (load AMCR data) will appear in the interface.*
|
|
||||||
|
|
||||||
## 3. User Manual
|
*Plugins → Manage and Install Plugins… → search for* **AMČR Viewer** *→
|
||||||
|
Install*.
|
||||||
|
|
||||||
### 3.1 Data Retrieval
|
### From a ZIP archive (older versions, or a build from source)
|
||||||
|
|
||||||
To initiate a search query, click either the **Stáhnout data akcí** or the **Stáhnout data lokalit** icon from the dropdown menu. The filter dialog provides the following options. Shown options vary based on the choosed tool.
|
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*.
|
||||||
|
|
||||||
* **Spatial Filter:** *Checkbox "Limit search to current map extent":* 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).
|
Every successful CI run also publishes a ready-to-install `amcr_viewer.zip`
|
||||||
* It is possible to view only those Fieldwork events with positive outcome, if "Positive findings only" is checked. Only *PIANs* marked as (or rather *PIANs* belonging to Documentation units marked as) "Type of evidence" = "positive" are rendered.
|
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.
|
||||||
|
|
||||||
* **Attribute Filters:**
|
### Requirements
|
||||||
* The dialog utilizes "Picker" widgets for controlled vocabularies (common: Region, District, Cadastral area, Period, Activity Area, *PIAN* accuracy; *events* related: Organisation, Researcher, Event type; *sites* related: Site type and class, Level of confidence, State of preservation).
|
|
||||||
* Click **Select...** to open a searchable selection window. Multiple values can be selected simultaneously (Logic: OR).
|
|
||||||
|
|
||||||
|
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`).
|
||||||
|
|
||||||
* **Fieldwork Manager (Dynamic List):**
|
---
|
||||||
* Due to the dynamic nature of the persons database, the list of Fieldwork Managers is retrieved from the AIS CR servers and needs to be updated the first time (and subsequently, if there is need).
|
|
||||||
* To refresh the list from the server, click the **Refresh (🔄)** button next to the selection field. This downloads the latest list of researchers from the API.
|
|
||||||
|
|
||||||
* **Components:** The *components* data are downloaded as well upon checking the corresponding check box. This enriches the main (*Events* and *Sites*) layers with additional information (period and activity area).
|
## 3. User manual
|
||||||
|
|
||||||
* If no filter is used, all accessible Fieldwork events/PIANs are returned (although the number of Fieldwork events to be loaded is capped at 20000 records; it is advisable to set at least one filter).
|
### 3.1 Toolbar and menu
|
||||||
|
|
||||||
For a more in-depth tutorial refer to the [AMČR Documentation](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
The toolbar button is a dropdown; the default action is *Stáhnout data akcí*.
|
||||||
|
|
||||||
### 3.2 Layer Structure & Attributes
|
| Menu entry | Action |
|
||||||
|
|
||||||
Upon successful retrieval, the plugin generates four temporary memory layers:
|
|
||||||
|
|
||||||
1. **AMCR Plochy (Polygons)**
|
|
||||||
2. **AMČR Linie (Lines)**
|
|
||||||
3. **AMČR Body (Points)**
|
|
||||||
4. **AMČR Komponenty (*Components*/no geometry)**
|
|
||||||
|
|
||||||
The Attribute Table includes standardized fields with important metadata. The components layer has no geometry on its own and depend solely on a relation with the other three layers.
|
|
||||||
|
|
||||||
#### 3.2.1 Fields of the layers with geometry
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| PIAN | PIAN (spatial identifier) ID |
|
| *Stáhnout data akcí* | Opens the filter dialog for Fieldwork events. |
|
||||||
| Přesnost | spatial deviation [in units/tens/hundreds of meters/defined by cadastre] |
|
| *Stáhnout data samostatných nálezů* | Opens the filter dialog for Individual finds. |
|
||||||
| PIAN – typ | [point/line/polygon] |
|
| *Stáhnout data lokalit* | Opens the filter dialog for Sites. |
|
||||||
| Dokumentační jednotka | Documentation unit ID |
|
| *Přihlásit se* | Opens the login dialog (see 3.2). |
|
||||||
| Typ dokumentační jednotky | [trench/event part/whole event/cadastral territory] |
|
| *Nápověda AMČR Help* | Opens the online documentation in a browser. |
|
||||||
| Definiční bod(y) (WGS-84) | feature centroid in WGS-84 coordinate system |
|
|
||||||
| Akce/Lokalita | Event/Site ID |
|
|
||||||
| Odkaz do Digitálního archivu AMČR | link to the Event/Site record in the Digital Archive |
|
|
||||||
| Okres | district |
|
|
||||||
| Katastr | main cadastre |
|
|
||||||
| Další katastr | other cadastres, if the event extends beyond the main cadastre |
|
|
||||||
| Přístupnost | record accessibility [A/B/C/D] |
|
|
||||||
|
|
||||||
> Common fields
|
### 3.2 Authentication (optional)
|
||||||
|
|
||||||
| Field | Description |
|
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
|
||||||
|
resources.py generated by pyrcc, currently unused
|
||||||
|
metadata.txt plugin metadata and changelog
|
||||||
|
.flake8 lint config, read by the plugins.qgis.org scanner
|
||||||
|
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 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Vedoucí akce | main fieldwork manager |
|
| **Lint a bezpečnost** | `tests/check_sources.py`, bandit, detect-secrets, flake8, ruff |
|
||||||
| Organizace | organization conducting the research |
|
| **Kompatibilita s Qt6** | `pyqgis4-checker` in dry-run mode |
|
||||||
| Specifikace data | [exact date/exact years/sometime in years] |
|
| **Smoke test** | loads the plugin in headless QGIS — both `ltr` (Qt 5) and `stable` (Qt 6) |
|
||||||
| Datum zahájení | Event start date |
|
| **Balíček pluginu** | builds `amcr_viewer.zip`, asserts its contents, uploads it as an artifact |
|
||||||
| Datum ukončení | Event end date |
|
|
||||||
| Hlavní typ | primary research method [total excavation/pit trench/surface collection survey/…] |
|
|
||||||
| Vedlejší typ | secondary research method [same options as in Hlavní typ] |
|
|
||||||
| Zjištění | did the research reveal archaeological contexts? [positive/negative] |
|
|
||||||
| Akce – lokalizace | verbal description of the event location |
|
|
||||||
| Akce – nahrazuje NZ | replaces a fieldwork report? [yes/no] |
|
|
||||||
|
|
||||||
> Fields related to *Fieldwork events*
|
Reproducing them locally:
|
||||||
|
|
||||||
| Field | Description |
|
```bash
|
||||||
| --- | --- |
|
python3 tests/check_sources.py
|
||||||
| nazev_lokality | site name |
|
ruff check .
|
||||||
| popis_lokality | site description |
|
flake8 --config amcr_viewer/.flake8 amcr_viewer/
|
||||||
| typ_lokality | site classification by definition method [survey polygon/heritage site/landscape] |
|
bandit -r amcr_viewer/
|
||||||
| druh_lokality | site classification by the nature of identified field relics [aerial survey polygon/landscape/remains of settlement/…] |
|
docker run --rm -v "$PWD:/work:ro" -w /work --user "$(id -u):$(id -g)" \
|
||||||
| zachovalost | site preservation state [buried site/ruin/aboveground remains/…] |
|
-e HOME=/tmp qgis/qgis:stable python3 tests/smoke_test.py
|
||||||
|
```
|
||||||
|
|
||||||
> Fields related to *Sites*
|
---
|
||||||
|
|
||||||
#### 3.2.2 Fields of the *Components* layer
|
## 6. Links and resources
|
||||||
|
|
||||||
| Field | Description |
|
* [AMČR / Digiarchiv documentation](https://amcr-help.aiscr.cz/) (Czech only)
|
||||||
| --- | --- |
|
* [AMČR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html)
|
||||||
| komponenta | Component ID |
|
(Czech only)
|
||||||
| dj_id | parent Documentation unit ID |
|
* [AMČR-PAS](https://amcr-info.aiscr.cz/amcr-pas/) — the amateur collaborator
|
||||||
| komponenta_areal | Activity area [settlement/burial area/field/…] |
|
portal behind the Individual finds records
|
||||||
| komponenta_obdobi | Period [Neolithic/High Middle Ages–Modern Period/Middle La Tène (LtB–C1)/…] |
|
* [Import/Export. Pluginy propojující QGIS s AMČR \[poster\]](https://zenodo.org/records/20504909)
|
||||||
| vrstva | system value linking to a specific geometry table with the corresponding documentation unit |
|
(Czech only; describes v1.3.2)
|
||||||
|
|
||||||
## 4. Technical Architecture
|
## Citing
|
||||||
|
|
||||||
The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
|
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.
|
||||||
|
|
||||||
### 4.1 File Structure
|
## Licence
|
||||||
|
|
||||||
* `amcr_viewer.py`: Entry point; handles GUI integration and initialization.
|
GPL-3.0 — see [`LICENSE`](./LICENSE).
|
||||||
* `amcr_dialog.py`: Manages the UI logic, including the custom `FilterableSelectionDialog` for handling large vocabularies.
|
|
||||||
* `amcr_tools.py`: Core logic module. Handles API requests, pagination, data parsing, and vector layer generation.
|
|
||||||
* `amcr_codelists.py`: Manages local caching of controlled vocabularies (`codelists/*.csv`).
|
|
||||||
|
|
||||||
### 4.2 Data Flow & API Integration
|
|
||||||
|
|
||||||
The plugin interacts with three primary endpoints of the AIS CR infrastructure:
|
|
||||||
|
|
||||||
1. **Search API (Solr):**
|
|
||||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/search/query`
|
|
||||||
* Method: `GET`
|
|
||||||
* Parameters: `entity=akce`, `rows/page` (pagination).
|
|
||||||
* Logic: The plugin implements a `while True` loop to handle pagination, processing data in batches of 500 records to ensure stability.
|
|
||||||
|
|
||||||
2. **Translation API:**
|
|
||||||
* Endpoint: `https://digiarchiv.aiscr.cz/api/assets/i18n/cs.json`
|
|
||||||
* Function: Retrieves the mapping between system codes (e.g., `HES-xxxx`) and Czech labels. This dictionary is cached in memory during the session.
|
|
||||||
|
|
||||||
### 4.3 Data Persistence
|
|
||||||
|
|
||||||
* **Vocabularies:** Static vocabularies (e.g., Periods, Regions) are stored in `codelists/heslar.csv`.
|
|
||||||
* **Dynamic Data:** The list of researchers is downloaded on-demand and cached in `codelists/vedouci.csv`.
|
|
||||||
* **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 (50 IDs per request) to comply with URL length limitations and server load balancing.
|
|
||||||
|
|
||||||
### 4.5 Relational Data Linking
|
|
||||||
The plugin automatically utilizes advanced QGIS features for data relationship management, specifically Polymorphic Relations. The *Components* layer is dynamically linked within the project to the spatial layers of events and sites via the documentation unit identifier (dj_id). This allows users to immediately see all *components* belonging to a given geometry (point, line, or polygon) directly in the attribute form or the identify features tool, without the need to manually filter or join the data.
|
|
||||||
|
|
||||||
## 5. Links and resources
|
|
||||||
|
|
||||||
* [AMCR/Digiarchive Documentation](https://amcr-help.aiscr.cz/) (only in Czech).
|
|
||||||
* [AMCR Viewer tutorial](https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html) (only in Czech).
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Konfigurace flake8 pro plugin AMČR Viewer.
|
||||||
|
#
|
||||||
|
# Soubor leží vedle metadata.txt schválně: scanner na plugins.qgis.org
|
||||||
|
# hledá .flake8 pouze v kořeni balíčku uvnitř ZIPu, takže stejná pravidla
|
||||||
|
# platí v CI, lokálně i při uploadu.
|
||||||
|
# https://plugins.qgis.org/docs/security-scanning/config-files
|
||||||
|
[flake8]
|
||||||
|
# resources.py je vygenerovaný výstup pyrcc ("All changes made in this
|
||||||
|
# file will be lost"), není nikde importovaný a zdrojový .qrc v repu není.
|
||||||
|
# Ručně se neformátuje.
|
||||||
|
per-file-ignores =
|
||||||
|
*resources.py: E302,E305,E501
|
||||||
@@ -31,6 +31,5 @@ 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)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 533 B |
+173
-80
@@ -1,32 +1,39 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
|
||||||
import csv
|
import csv
|
||||||
import requests
|
import os
|
||||||
import xml.etree.ElementTree as ET # nosec B314
|
|
||||||
import time
|
import time
|
||||||
from qgis.core import QgsMessageLog, Qgis
|
import xml.etree.ElementTree as ET # nosec
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from qgis.core import Qgis, QgsMessageLog
|
||||||
|
|
||||||
# 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__)
|
||||||
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
|
CODELISTS_DIR = os.path.join(PLUGIN_DIR, 'codelists')
|
||||||
BASE_URL = "https://api.aiscr.cz/2.2/oai"
|
BASE_URL_AMCR = "https://api.aiscr.cz/2.2/oai"
|
||||||
|
BASE_URL_DA = "https://digiarchiv.aiscr.cz/api/search/query"
|
||||||
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
OUTPUT_FILE = os.path.join(CODELISTS_DIR, 'heslar.csv')
|
||||||
|
|
||||||
slovnicek = {
|
slovnicek = {
|
||||||
'obdobi' : 'heslo:obdobi',
|
'obdobi': (BASE_URL_AMCR, 'heslo:obdobi'),
|
||||||
'typ_akce' : 'heslo:akce_typ',
|
'typ_akce': (BASE_URL_AMCR, 'heslo:akce_typ'),
|
||||||
'areal' : 'heslo:areal',
|
'areal': (BASE_URL_AMCR, 'heslo:areal'),
|
||||||
'kraj' : 'ruian_kraj',
|
'kraj': (BASE_URL_AMCR, 'ruian_kraj'),
|
||||||
'organizace' : 'organizace',
|
'organizace': (BASE_URL_AMCR, 'organizace'),
|
||||||
'okres' : 'ruian_okres',
|
'okres': (BASE_URL_AMCR, 'ruian_okres'),
|
||||||
'katastr' : 'ruian_katastr',
|
'katastr': (BASE_URL_AMCR, 'ruian_katastr'),
|
||||||
'vedouci' : 'osoba',
|
'pian_presnost': (BASE_URL_AMCR, 'heslo:pian_presnost'),
|
||||||
'pian_presnost' : 'heslo:pian_presnost',
|
'typ_lokality': (BASE_URL_AMCR, 'heslo:lokalita_typ'),
|
||||||
'typ_lokality' : 'heslo:lokalita_typ',
|
'druh_lokality': (BASE_URL_AMCR, 'heslo:lokalita_druh'),
|
||||||
'druh_lokality' : 'heslo:lokalita_druh',
|
'jistota': (BASE_URL_AMCR, 'heslo:jistota_urceni'),
|
||||||
'jistota' : 'heslo:jistota_urceni',
|
'lokalita_zachovalost': (BASE_URL_AMCR, 'heslo:stav_dochovani'),
|
||||||
'lokalita_zachovalost' : 'heslo:stav_dochovani',
|
'pristupnost': (BASE_URL_AMCR, 'heslo:pristupnost'),
|
||||||
'pristupnost' : 'heslo:pristupnost'
|
'nalez_kategorie': (BASE_URL_AMCR, 'heslo:predmet_druh_kat'),
|
||||||
|
'druh_nalezu': (BASE_URL_AMCR, 'heslo:predmet_druh'),
|
||||||
|
'specifikace': (BASE_URL_AMCR, 'heslo:predmet_specifikace'),
|
||||||
|
'nalezove_okolnosti': (BASE_URL_AMCR, 'heslo:nalezove_okolnosti'),
|
||||||
|
'vedouci': (BASE_URL_DA, 'f_vedouci'),
|
||||||
|
'nalezce': (BASE_URL_DA, 'f_nalezce'),
|
||||||
}
|
}
|
||||||
|
|
||||||
NS = {
|
NS = {
|
||||||
@@ -35,13 +42,18 @@ NS = {
|
|||||||
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def ensure_codelists_dir():
|
def ensure_codelists_dir():
|
||||||
"""Creates the codelists directory if it does not exist."""
|
"""Creates the codelists directory if it does not exist."""
|
||||||
if not os.path.exists(CODELISTS_DIR):
|
if not os.path.exists(CODELISTS_DIR):
|
||||||
os.makedirs(CODELISTS_DIR)
|
os.makedirs(CODELISTS_DIR)
|
||||||
|
|
||||||
|
|
||||||
def parse_codelist_file(filename, target_dict=None):
|
def parse_codelist_file(filename, target_dict=None):
|
||||||
"""Reads a CSV codelist file and populates the target dictionary grouped by categories."""
|
"""
|
||||||
|
Reads a CSV codelist file and populates
|
||||||
|
the target dictionary grouped by categories.
|
||||||
|
"""
|
||||||
if target_dict is None:
|
if target_dict is None:
|
||||||
target_dict = {}
|
target_dict = {}
|
||||||
|
|
||||||
@@ -53,7 +65,7 @@ def parse_codelist_file(filename, target_dict=None):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Open the file using standard UTF-8 encoding
|
# Open the file using standard UTF-8 encoding
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, encoding='utf-8') as f:
|
||||||
reader = csv.reader(f, delimiter=';')
|
reader = csv.reader(f, delimiter=';')
|
||||||
|
|
||||||
# Skip the CSV header row
|
# Skip the CSV header row
|
||||||
@@ -67,118 +79,186 @@ def parse_codelist_file(filename, target_dict=None):
|
|||||||
cat = row[2].strip()
|
cat = row[2].strip()
|
||||||
clean = code if code else None
|
clean = code if code else None
|
||||||
|
|
||||||
# Initialize a new dictionary for a category if encountered for the first time
|
# Initialize a new dictionary for a category if encountered
|
||||||
|
# for the first time
|
||||||
if cat not in target_dict:
|
if cat not in target_dict:
|
||||||
target_dict[cat] = {}
|
target_dict[cat] = {}
|
||||||
|
|
||||||
# Assign the extracted code to the corresponding label within the category
|
# Assign the extracted code to the corresponding label
|
||||||
|
# within the category
|
||||||
target_dict[cat][label] = clean
|
target_dict[cat][label] = clean
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QgsMessageLog.logMessage(f"AMČR Codelist Read Error for {filename}: {e}", "AMČR", Qgis.Critical)
|
QgsMessageLog.logMessage(
|
||||||
|
f"AMČR Codelist Read Error for {filename}: {e}",
|
||||||
|
"AMČR", Qgis.MessageLevel.Critical)
|
||||||
|
|
||||||
return target_dict
|
return target_dict
|
||||||
|
|
||||||
|
|
||||||
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.keys()}
|
categorized_data = {k: {} for k in slovnicek}
|
||||||
parse_codelist_file('heslar.csv', categorized_data)
|
parse_codelist_file('heslar.csv', categorized_data)
|
||||||
return categorized_data
|
return categorized_data
|
||||||
|
|
||||||
def fetch_set(internal_name, api_set, task=None):
|
|
||||||
|
def fetch_set(base_url, internal_name, api_set, task=None):
|
||||||
dataset = []
|
dataset = []
|
||||||
params = {
|
params_amcr = {
|
||||||
"verb": "ListRecords",
|
"verb": "ListRecords",
|
||||||
"metadataPrefix": "oai_dc",
|
"metadataPrefix": "oai_dc",
|
||||||
"set": api_set
|
"set": api_set
|
||||||
}
|
}
|
||||||
|
params_da = {
|
||||||
|
"entity": "samostatny_nalez" if internal_name == "nalezce" else "akce",
|
||||||
|
"rows": 0,
|
||||||
|
"noFacets": "false",
|
||||||
|
"onlyFacets": "true"
|
||||||
|
}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Kontrola zrušení v každém kroku
|
# Check for cancellation at each iteration
|
||||||
if task and task.isCanceled():
|
if task and task.isCanceled():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(BASE_URL, params=params, timeout=30)
|
if "digiarchiv" not in base_url:
|
||||||
response.raise_for_status()
|
response = requests.get(
|
||||||
root = ET.fromstring(response.content)
|
base_url, params=params_amcr, timeout=30
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
root = ET.fromstring(response.content) # nosec
|
||||||
|
|
||||||
records = root.findall('.//oai:record', NS)
|
records = root.findall('.//oai:record', NS)
|
||||||
for rec in records:
|
for rec in records:
|
||||||
metadata = rec.find('.//oai_dc:dc', NS)
|
metadata = rec.find('.//oai_dc:dc', NS)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
# Kód (identifier)
|
# Code (identifier)
|
||||||
kod = metadata.find('dc:identifier', NS).text if metadata.find('dc:identifier', NS) is not None else ""
|
identifier_el = metadata.find('dc:identifier', NS)
|
||||||
|
kod = (
|
||||||
|
identifier_el.text
|
||||||
|
if identifier_el is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Název (title) - filtrujeme systémové popisky "AMČR - ..."
|
# Title – filter out system labels "AMČR - ..."
|
||||||
titles = metadata.findall('dc:title', NS)
|
titles = metadata.findall('dc:title', NS)
|
||||||
nazev = ""
|
nazev = ""
|
||||||
for t in titles:
|
for t in titles:
|
||||||
if t.text and not t.text.startswith("AMČR -") and not t.text.startswith(" AMČR -"):
|
if (
|
||||||
nazev = t.text
|
t.text
|
||||||
break
|
and not t.text.startswith("AMČR -")
|
||||||
# Pokud by náhodou žádný title neprošel filtrem, vezmeme první dostupný
|
and not t.text.startswith(" AMČR -")
|
||||||
if not nazev and titles:
|
):
|
||||||
nazev = titles[0].text
|
nazev = t.text
|
||||||
|
break
|
||||||
|
# If no title passed the filter, fall back
|
||||||
|
# to the first available one
|
||||||
|
if not nazev and titles:
|
||||||
|
nazev = titles[0].text
|
||||||
|
|
||||||
specialni_pripady = ['okres', 'katastr']
|
specialni_pripady = ['okres', 'katastr']
|
||||||
|
|
||||||
if internal_name in specialni_pripady:
|
if internal_name in specialni_pripady:
|
||||||
kod = nazev
|
kod = nazev
|
||||||
|
|
||||||
if internal_name == 'pristupnost':
|
if internal_name == 'pristupnost':
|
||||||
kod = next((t.text for t in titles if t.text and len(t.text) == 1 and t.text.isalpha()), None)
|
kod = next(
|
||||||
|
(
|
||||||
|
t.text for t in titles
|
||||||
|
if t.text
|
||||||
|
and len(t.text) == 1
|
||||||
|
and t.text.isalpha()
|
||||||
|
),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
# Skip records without a valid one-letter code –
|
||||||
|
# a None code would end up in the CSV and later
|
||||||
|
# in the API filter as the string "None"
|
||||||
|
if not kod:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dataset.append({
|
||||||
|
'Název': nazev,
|
||||||
|
'Kód': kod,
|
||||||
|
'Kategorie': internal_name
|
||||||
|
})
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
token = root.find('.//oai:resumptionToken', NS)
|
||||||
|
if token is not None and token.text:
|
||||||
|
params_amcr = {
|
||||||
|
"verb": "ListRecords",
|
||||||
|
"resumptionToken": token.text
|
||||||
|
}
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
response = requests.get(base_url, params=params_da, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data_json = response.json()
|
||||||
|
|
||||||
|
records = data_json['facet_counts']['facet_fields'][api_set]
|
||||||
|
|
||||||
|
for r in records:
|
||||||
|
|
||||||
|
nazev = r["name"]
|
||||||
|
|
||||||
dataset.append({
|
dataset.append({
|
||||||
'Název': nazev,
|
'Název': nazev,
|
||||||
'Kód': kod,
|
'Kód': nazev,
|
||||||
'Kategorie': internal_name
|
'Kategorie': internal_name
|
||||||
})
|
})
|
||||||
|
|
||||||
# Stránkování
|
|
||||||
token = root.find('.//oai:resumptionToken', NS)
|
|
||||||
if token is not None and token.text:
|
|
||||||
params = {
|
|
||||||
"verb": "ListRecords",
|
|
||||||
"resumptionToken": token.text
|
|
||||||
}
|
|
||||||
time.sleep(0.5)
|
|
||||||
else:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QgsMessageLog.logMessage(f"Chyba u setu {api_set}: {e}", "AMČR", Qgis.Warning)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Chyba u setu {api_set}: {e}",
|
||||||
|
"AMČR", Qgis.MessageLevel.Warning)
|
||||||
break
|
break
|
||||||
|
|
||||||
return dataset
|
return dataset
|
||||||
|
|
||||||
|
|
||||||
def download_heslare(task=None):
|
def download_heslare(task=None):
|
||||||
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
"""Fetches the codelists from the AMČR API and saves it to a CSV file."""
|
||||||
ensure_codelists_dir()
|
ensure_codelists_dir()
|
||||||
all_data = []
|
all_data = []
|
||||||
total_sets = len(slovnicek)
|
total_sets = len(slovnicek)
|
||||||
|
# index, (interni, api_nazev)
|
||||||
|
for index, (key, value) in enumerate(slovnicek.items()):
|
||||||
|
|
||||||
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
|
base_url = value[0]
|
||||||
# Pokud uživatel task zrušil v liště QGISu
|
interni = key
|
||||||
|
api_nazev = value[1]
|
||||||
|
|
||||||
|
# Check if the user cancelled the task via the QGIS taskbar
|
||||||
if task and task.isCanceled():
|
if task and task.isCanceled():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
QgsMessageLog.logMessage(f"Zpracovávám kategorii: {interni}...", "AMČR", Qgis.Info)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Zpracovávám kategorii: {interni}...",
|
||||||
|
"AMČR", Qgis.MessageLevel.Info)
|
||||||
|
|
||||||
# Nyní předáváme task správně do upravené funkce
|
# Pass the task correctly to the updated fetch function
|
||||||
data = fetch_set(interni, api_nazev, task=task)
|
data = fetch_set(base_url, interni, api_nazev, task=task)
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return False # Bylo zrušeno uprostřed stahování
|
return False # Cancelled mid-download
|
||||||
|
|
||||||
all_data.extend(data)
|
all_data.extend(data)
|
||||||
|
|
||||||
# Reportování postupu (0-100)
|
# Report progress (0-100)
|
||||||
if task:
|
if task:
|
||||||
progress = (index + 1) / total_sets * 100
|
progress = (index + 1) / total_sets * 100
|
||||||
task.setProgress(progress)
|
task.setProgress(progress)
|
||||||
|
|
||||||
# Uložení do CSV
|
# Save to CSV
|
||||||
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
|
with open(OUTPUT_FILE, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
fieldnames = ['Název', 'Kód', 'Kategorie']
|
fieldnames = ['Název', 'Kód', 'Kategorie']
|
||||||
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
|
||||||
@@ -187,11 +267,9 @@ def download_heslare(task=None):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def refresh_globals():
|
|
||||||
"""Znovu načte data ze souborů do globálních proměnných."""
|
|
||||||
global OBDOBI, TYP_AKCE, AREAL, KRAJE, ORGANIZACE, OKRESY, KATASTRY
|
|
||||||
global VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY, DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST
|
|
||||||
|
|
||||||
|
def refresh_globals():
|
||||||
|
"""Reloads data from files into the global variables."""
|
||||||
data = load_all_data()
|
data = load_all_data()
|
||||||
|
|
||||||
OBDOBI.clear()
|
OBDOBI.clear()
|
||||||
@@ -222,9 +300,19 @@ def refresh_globals():
|
|||||||
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
|
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
|
||||||
PRISTUPNOST.clear()
|
PRISTUPNOST.clear()
|
||||||
PRISTUPNOST.update(data.get('pristupnost', {}))
|
PRISTUPNOST.update(data.get('pristupnost', {}))
|
||||||
|
NALEZ_KATEGORIE.clear()
|
||||||
|
NALEZ_KATEGORIE.update(data.get('nalez_kategorie', {}))
|
||||||
|
DRUH_NALEZU.clear()
|
||||||
|
DRUH_NALEZU.update(data.get('druh_nalezu', {}))
|
||||||
|
SPECIFIKACE.clear()
|
||||||
|
SPECIFIKACE.update(data.get('specifikace', {}))
|
||||||
|
NALEZOVE_OKOLNOSTI.clear()
|
||||||
|
NALEZOVE_OKOLNOSTI.update(data.get('nalezove_okolnosti', {}))
|
||||||
|
NALEZCE.clear()
|
||||||
|
NALEZCE.update(data.get('nalezce', {}))
|
||||||
|
|
||||||
|
|
||||||
# Inicializace prázdných diktů, které se naplní hned pod tím
|
# Initialize empty dicts that will be populated immediately below
|
||||||
OBDOBI = {}
|
OBDOBI = {}
|
||||||
TYP_AKCE = {}
|
TYP_AKCE = {}
|
||||||
AREAL = {}
|
AREAL = {}
|
||||||
@@ -239,5 +327,10 @@ DRUH_LOKALITY = {}
|
|||||||
JISTOTA = {}
|
JISTOTA = {}
|
||||||
LOKALITA_ZACHOVALOST = {}
|
LOKALITA_ZACHOVALOST = {}
|
||||||
PRISTUPNOST = {}
|
PRISTUPNOST = {}
|
||||||
|
NALEZ_KATEGORIE = {}
|
||||||
|
DRUH_NALEZU = {}
|
||||||
|
SPECIFIKACE = {}
|
||||||
|
NALEZOVE_OKOLNOSTI = {}
|
||||||
|
NALEZCE = {}
|
||||||
|
|
||||||
refresh_globals()
|
refresh_globals()
|
||||||
+586
-110
@@ -1,26 +1,85 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
from qgis.core import (
|
||||||
QLineEdit, QDialogButtonBox,
|
Qgis,
|
||||||
QCheckBox, QGroupBox, QPushButton,
|
QgsApplication,
|
||||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
QgsAuthMethodConfig,
|
||||||
QMessageBox, QLabel, QFormLayout)
|
QgsMessageLog,
|
||||||
from qgis.PyQt.QtCore import Qt, QSettings
|
QgsTask,
|
||||||
from qgis.core import QgsTask, QgsApplication, QgsMessageLog, Qgis, QgsAuthMethodConfig
|
)
|
||||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
from qgis.gui import QgsDateEdit
|
||||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST, TYP_LOKALITY,
|
from qgis.PyQt.QtCore import QSettings, Qt
|
||||||
DRUH_LOKALITY, JISTOTA, LOKALITA_ZACHOVALOST, PRISTUPNOST,
|
from qgis.PyQt.QtWidgets import (
|
||||||
download_heslare, refresh_globals)
|
QCheckBox,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFormLayout,
|
||||||
|
QFrame,
|
||||||
|
QGridLayout,
|
||||||
|
QGroupBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QListWidget,
|
||||||
|
QListWidgetItem,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QScrollArea,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
# Keep Python references to running tasks. QgsTaskManager only holds the
|
||||||
|
# C++ object; without a Python-side reference the wrapper can be garbage
|
||||||
|
# collected before the task finishes, which crashes QGIS.
|
||||||
|
_ACTIVE_TASKS = []
|
||||||
|
|
||||||
|
|
||||||
class UpdateCodelistsTask(QgsTask):
|
class UpdateCodelistsTask(QgsTask):
|
||||||
def __init__(self, description):
|
def __init__(self, description):
|
||||||
super().__init__(description, QgsTask.CanCancel)
|
super().__init__(description, QgsTask.Flag.CanCancel)
|
||||||
self.success = False
|
self.success = False
|
||||||
self.exception = None
|
self.exception = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Tato část běží ve vedlejším vlákně."""
|
"""Runs in a background thread."""
|
||||||
try:
|
try:
|
||||||
# Voláme upravenou funkci
|
# Call the download function with the task reference
|
||||||
self.success = download_heslare(task=self)
|
self.success = download_heslare(task=self)
|
||||||
return self.success
|
return self.success
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -28,20 +87,31 @@ class UpdateCodelistsTask(QgsTask):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def finished(self, result):
|
def finished(self, result):
|
||||||
"""Tato část běží v hlavním vlákně po skončení run()."""
|
"""Runs in the main thread after run() completes."""
|
||||||
if result:
|
if result:
|
||||||
# Teď bezpečně aktualizujeme globální proměnné v hlavním vlákně
|
# Safely update the global variables in the main thread
|
||||||
refresh_globals()
|
refresh_globals()
|
||||||
QgsMessageLog.logMessage("Hesláře AMČR byly úspěšně aktualizovány.", "AMČR", Qgis.Info)
|
QgsMessageLog.logMessage(
|
||||||
|
"Hesláře AMČR byly úspěšně aktualizovány.",
|
||||||
|
"AMČR", Qgis.MessageLevel.Info
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if self.isCanceled():
|
if self.isCanceled():
|
||||||
QgsMessageLog.logMessage("Aktualizace heslářů byla zrušena.", "AMČR", Qgis.Warning)
|
QgsMessageLog.logMessage(
|
||||||
|
"Aktualizace heslářů byla zrušena.",
|
||||||
|
"AMČR", Qgis.MessageLevel.Warning
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
QgsMessageLog.logMessage(f"Chyba aktualizace: {self.exception}", "AMČR", Qgis.Critical)
|
QgsMessageLog.logMessage(
|
||||||
|
f"Chyba aktualizace: {self.exception}",
|
||||||
|
"AMČR", Qgis.MessageLevel.Critical
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FilterableSelectionDialog(QDialog):
|
class FilterableSelectionDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
A custom dialog for selecting multiple items from a list with a search filter.
|
A custom dialog for selecting multiple items from
|
||||||
|
a list with a search filter.
|
||||||
Updated for PyQt6/Qt6 compatibility.
|
Updated for PyQt6/Qt6 compatibility.
|
||||||
"""
|
"""
|
||||||
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
def __init__(self, title, data_dict, preselected_codes, parent=None):
|
||||||
@@ -68,7 +138,8 @@ class FilterableSelectionDialog(QDialog):
|
|||||||
|
|
||||||
# Standard OK/Cancel dialog buttons
|
# Standard OK/Cancel dialog buttons
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
@@ -126,17 +197,37 @@ class AmcrFilterDialog(QDialog):
|
|||||||
self.setWindowTitle("Filtr AMČR")
|
self.setWindowTitle("Filtr AMČR")
|
||||||
self.resize(500, 750)
|
self.resize(500, 750)
|
||||||
|
|
||||||
# Determines if we are fetching 'akce' (projects) or 'lokalita' (locations)
|
# Determines if we are fetching 'akce' (events)
|
||||||
|
# or 'lokalita' (sites)
|
||||||
self.typ_dat = typ_dat
|
self.typ_dat = typ_dat
|
||||||
|
|
||||||
|
|
||||||
# Cache dictionary to store selected codes for each category
|
# Cache dictionary to store selected codes for each category
|
||||||
self.selection_cache = {
|
self.selection_cache = {
|
||||||
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
|
'organizace': [],
|
||||||
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [], 'pian_presnost': [], 'pristupnost': [],
|
'kraj': [],
|
||||||
'typ_lokality': [], 'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
|
'obdobi': [],
|
||||||
|
'areal': [],
|
||||||
|
'typ_akce': [],
|
||||||
|
'okres': [],
|
||||||
|
'katastr': [],
|
||||||
|
'vedouci': [],
|
||||||
|
'pian_presnost': [],
|
||||||
|
'pristupnost': [],
|
||||||
|
'typ_lokality': [],
|
||||||
|
'druh_lokality': [],
|
||||||
|
'jistota': [],
|
||||||
|
'lokalita_zachovalost': [],
|
||||||
|
'nalez_kategorie': [],
|
||||||
|
'druh_nalezu': [],
|
||||||
|
'specifikace': [],
|
||||||
|
'nalezove_okolnosti': [],
|
||||||
|
'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
|
||||||
@@ -149,6 +240,8 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
|
||||||
layout.addWidget(self.chk_posevidence)
|
layout.addWidget(self.chk_posevidence)
|
||||||
|
self.chk_proj_akce = QCheckBox("Pouze projektové akce")
|
||||||
|
layout.addWidget(self.chk_proj_akce)
|
||||||
|
|
||||||
layout.addSpacing(10)
|
layout.addSpacing(10)
|
||||||
|
|
||||||
@@ -160,42 +253,90 @@ class AmcrFilterDialog(QDialog):
|
|||||||
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
self.picker_okres = self.setup_picker("Okres", 'okres', OKRESY)
|
||||||
layout.addWidget(self.picker_okres)
|
layout.addWidget(self.picker_okres)
|
||||||
|
|
||||||
self.picker_katastr = self.setup_picker("Katastr", 'katastr', KATASTRY)
|
self.picker_katastr = self.setup_picker(
|
||||||
|
"Katastr",
|
||||||
|
'katastr',
|
||||||
|
KATASTRY
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_katastr)
|
layout.addWidget(self.picker_katastr)
|
||||||
|
|
||||||
self.picker_presnost = self.setup_picker("PIAN – přesnost", 'pian_presnost', PIAN_PRESNOST)
|
self.picker_pristupnost = self.setup_picker(
|
||||||
layout.addWidget(self.picker_presnost)
|
"Přístupnost",
|
||||||
|
'pristupnost',
|
||||||
self.picker_pristupnost = self.setup_picker("Přístupnost", 'pristupnost', PRISTUPNOST)
|
PRISTUPNOST
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_pristupnost)
|
layout.addWidget(self.picker_pristupnost)
|
||||||
|
|
||||||
# Filters valid for Akce
|
# Filters valid for Akce
|
||||||
|
|
||||||
if self.typ_dat == "akce":
|
if self.typ_dat in ["lokalita", "akce"]:
|
||||||
self.picker_org = self.setup_picker("Organizace", 'organizace', ORGANIZACE)
|
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(
|
||||||
|
"Organizace",
|
||||||
|
'organizace',
|
||||||
|
ORGANIZACE
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_org)
|
layout.addWidget(self.picker_org)
|
||||||
|
|
||||||
self.picker_vedouci = self.setup_picker("Vedoucí výzkumu", 'vedouci', VEDOUCI)
|
if self.typ_dat == "akce":
|
||||||
|
self.picker_vedouci = self.setup_picker(
|
||||||
|
"Vedoucí výzkumu",
|
||||||
|
'vedouci',
|
||||||
|
VEDOUCI
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_vedouci)
|
layout.addWidget(self.picker_vedouci)
|
||||||
|
|
||||||
# Type of event
|
# Type of event
|
||||||
|
|
||||||
self.picker_typ = self.setup_picker("Typ výzkumu", 'typ_akce', TYP_AKCE)
|
self.picker_typ = self.setup_picker(
|
||||||
|
"Typ výzkumu",
|
||||||
|
'typ_akce',
|
||||||
|
TYP_AKCE
|
||||||
|
)
|
||||||
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":
|
||||||
self.picker_typ_lokality = self.setup_picker("Lokalita – typ", 'typ_lokality', TYP_LOKALITY)
|
self.picker_typ_lokality = self.setup_picker(
|
||||||
|
"Lokalita – typ",
|
||||||
|
'typ_lokality',
|
||||||
|
TYP_LOKALITY
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_typ_lokality)
|
layout.addWidget(self.picker_typ_lokality)
|
||||||
|
|
||||||
self.picker_druh_lokality = self.setup_picker("Lokalita – druh", 'druh_lokality', DRUH_LOKALITY)
|
self.picker_druh_lokality = self.setup_picker(
|
||||||
|
"Lokalita – druh",
|
||||||
|
'druh_lokality',
|
||||||
|
DRUH_LOKALITY
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_druh_lokality)
|
layout.addWidget(self.picker_druh_lokality)
|
||||||
|
|
||||||
self.picker_jistota = self.setup_picker("Lokalita – jistota určení", 'jistota', JISTOTA)
|
self.picker_jistota = self.setup_picker(
|
||||||
|
"Lokalita – jistota určení",
|
||||||
|
'jistota',
|
||||||
|
JISTOTA
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_jistota)
|
layout.addWidget(self.picker_jistota)
|
||||||
|
|
||||||
self.picker_lokalita_zachovalost = self.setup_picker("Lokalita - stav dochování", 'lokalita_zachovalost', LOKALITA_ZACHOVALOST)
|
self.picker_lokalita_zachovalost = self.setup_picker(
|
||||||
|
"Lokalita - stav dochování",
|
||||||
|
'lokalita_zachovalost',
|
||||||
|
LOKALITA_ZACHOVALOST
|
||||||
|
)
|
||||||
layout.addWidget(self.picker_lokalita_zachovalost)
|
layout.addWidget(self.picker_lokalita_zachovalost)
|
||||||
|
|
||||||
# Contextual information
|
# Contextual information
|
||||||
@@ -203,38 +344,123 @@ 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)
|
||||||
|
|
||||||
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
if self.typ_dat == "samostatny_nalez":
|
||||||
layout.addWidget(self.picker_areal)
|
self.picker_nalez_kategorie = self.setup_picker(
|
||||||
|
"Kategorie nálezu",
|
||||||
|
'nalez_kategorie',
|
||||||
|
NALEZ_KATEGORIE
|
||||||
|
)
|
||||||
|
layout.addWidget(self.picker_nalez_kategorie)
|
||||||
|
|
||||||
# Option to download related components table
|
self.picker_druh_nalezu = self.setup_picker(
|
||||||
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
"Druh nálezu",
|
||||||
layout.addWidget(self.chk_komponenty)
|
'druh_nalezu',
|
||||||
|
DRUH_NALEZU
|
||||||
|
)
|
||||||
|
layout.addWidget(self.picker_druh_nalezu)
|
||||||
|
|
||||||
|
self.picker_specifikace = self.setup_picker(
|
||||||
|
"Specifikace nálezu",
|
||||||
|
'specifikace',
|
||||||
|
SPECIFIKACE
|
||||||
|
)
|
||||||
|
layout.addWidget(self.picker_specifikace)
|
||||||
|
|
||||||
|
self.picker_nalezove_okolnosti = self.setup_picker(
|
||||||
|
"Okolnosti nálezu",
|
||||||
|
'nalezove_okolnosti',
|
||||||
|
NALEZOVE_OKOLNOSTI
|
||||||
|
)
|
||||||
|
layout.addWidget(self.picker_nalezove_okolnosti)
|
||||||
|
|
||||||
|
self.picker_nalezce = self.setup_picker(
|
||||||
|
"Nálezce",
|
||||||
|
'nalezce',
|
||||||
|
NALEZCE
|
||||||
|
)
|
||||||
|
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":
|
||||||
|
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
||||||
|
layout.addWidget(self.picker_areal)
|
||||||
|
|
||||||
|
# Option to download related components table
|
||||||
|
self.chk_komponenty = QCheckBox("Načíst komponenty")
|
||||||
|
layout.addWidget(self.chk_komponenty)
|
||||||
|
|
||||||
|
# Warning label
|
||||||
|
self.lbl_komponenty_warning = QLabel(
|
||||||
|
"⚠ Při načtení komponent jsou prostorové prvky duplikovány — "
|
||||||
|
"každý prvek odpovídá jedné komponentě. "
|
||||||
|
"Prostorové analýzy (plochy, počty) mohou být zkreslené."
|
||||||
|
)
|
||||||
|
self.lbl_komponenty_warning.setWordWrap(True)
|
||||||
|
self.lbl_komponenty_warning.setStyleSheet(
|
||||||
|
"color: #8a6d00; background-color: #fff8e1; "
|
||||||
|
"border: 1px solid #ffe082; border-radius: 4px; padding: 6px;"
|
||||||
|
)
|
||||||
|
self.lbl_komponenty_warning.setVisible(False)
|
||||||
|
layout.addWidget(self.lbl_komponenty_warning)
|
||||||
|
|
||||||
|
if self.typ_dat != "samostatny_nalez":
|
||||||
|
self.chk_komponenty.toggled.connect(
|
||||||
|
self.lbl_komponenty_warning.setVisible
|
||||||
|
)
|
||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
|
self.btn_update = QPushButton("Aktualizovat hesláře 🔄")
|
||||||
self.btn_update.setToolTip("Provede kompletní aktualizaci heslářů AMČR. Toto bude trvat pár minut.")
|
self.btn_update.setToolTip(
|
||||||
|
"Provede kompletní aktualizaci heslářů AMČR. "
|
||||||
|
"Toto bude trvat pár minut."
|
||||||
|
)
|
||||||
self.btn_update.clicked.connect(self.action_update_heslare)
|
self.btn_update.clicked.connect(self.action_update_heslare)
|
||||||
|
|
||||||
buttons.addButton(self.btn_update, QDialogButtonBox.ButtonRole.ActionRole)
|
buttons.addButton(
|
||||||
|
self.btn_update,
|
||||||
|
QDialogButtonBox.ButtonRole.ActionRole
|
||||||
|
)
|
||||||
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
|
buttons.addButton(QDialogButtonBox.StandardButton.Ok)
|
||||||
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
buttons.addButton(QDialogButtonBox.StandardButton.Cancel)
|
||||||
|
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addWidget(buttons)
|
outer.addWidget(buttons)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(outer)
|
||||||
|
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
Creates a reusable UI component consisting of a label, a read-only
|
Creates a reusable UI component consisting of a label, a read-only
|
||||||
text field showing selected items, and a button to open the selection dialog.
|
text field showing selected items, and a button to open
|
||||||
|
the selection dialog.
|
||||||
"""
|
"""
|
||||||
row_widget = QGroupBox(label_text)
|
row_widget = QGroupBox(label_text)
|
||||||
row_layout = QHBoxLayout()
|
row_layout = QHBoxLayout()
|
||||||
@@ -249,55 +475,180 @@ class AmcrFilterDialog(QDialog):
|
|||||||
btn = QPushButton("Vybrat...")
|
btn = QPushButton("Vybrat...")
|
||||||
btn.setFixedWidth(80)
|
btn.setFixedWidth(80)
|
||||||
|
|
||||||
# Nested function that handles opening the dialog and saving results
|
# Nested handler: opens the selection dialog and saves the result
|
||||||
def open_dialog():
|
def open_dialog():
|
||||||
dlg = FilterableSelectionDialog(label_text, data_source, self.selection_cache[cache_key], self)
|
dlg = FilterableSelectionDialog(
|
||||||
|
label_text,
|
||||||
|
data_source,
|
||||||
|
self.selection_cache[cache_key],
|
||||||
|
self
|
||||||
|
)
|
||||||
if dlg.exec() == QDialog.DialogCode.Accepted:
|
if dlg.exec() == QDialog.DialogCode.Accepted:
|
||||||
codes, labels = dlg.get_selected_codes()
|
codes, labels = dlg.get_selected_codes()
|
||||||
# Update local cache with selected IDs
|
# Update the local cache with selected IDs
|
||||||
self.selection_cache[cache_key] = codes
|
self.selection_cache[cache_key] = codes
|
||||||
# Update the UI text field with selected names
|
# Update the display field with the selected item names
|
||||||
if labels:
|
if labels:
|
||||||
display_field.setText(", ".join(labels))
|
display_field.setText(", ".join(labels))
|
||||||
else:
|
else:
|
||||||
display_field.clear()
|
display_field.clear()
|
||||||
|
|
||||||
# Special case: Pre-fill specific accuracy levels by default
|
# Special case: pre-select default PIAN accuracy levels
|
||||||
if cache_key == 'pian_presnost':
|
if cache_key == 'pian_presnost':
|
||||||
display_field.setText("odchylka jednotky metrů, odchylka desítky metrů, odchylka stovky metrů")
|
display_field.setText(
|
||||||
self.selection_cache[cache_key] = ['HES-000861', 'HES-000862', 'HES-000863']
|
"odchylka jednotky metrů, odchylka desítky metrů, "
|
||||||
|
"odchylka stovky metrů"
|
||||||
|
)
|
||||||
|
self.selection_cache[cache_key] = [
|
||||||
|
'HES-000861',
|
||||||
|
'HES-000862',
|
||||||
|
'HES-000863',
|
||||||
|
]
|
||||||
|
|
||||||
btn.clicked.connect(open_dialog)
|
btn.clicked.connect(open_dialog)
|
||||||
|
|
||||||
row_layout.addWidget(display_field)
|
row_layout.addWidget(display_field)
|
||||||
row_layout.addWidget(btn)
|
row_layout.addWidget(btn)
|
||||||
|
|
||||||
# Add an optional extra button (e.g., the refresh button for leaders)
|
# Optionally append an extra button (e.g. a refresh button)
|
||||||
if extra_btn:
|
if extra_btn:
|
||||||
row_layout.addWidget(extra_btn)
|
row_layout.addWidget(extra_btn)
|
||||||
|
|
||||||
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):
|
||||||
# Vytvoření instance tasku
|
# Create the task instance and keep a reference so the Python
|
||||||
|
# wrapper survives until the task finishes
|
||||||
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
|
||||||
|
_ACTIVE_TASKS.append(task)
|
||||||
|
|
||||||
# Povolíme tlačítko zpět bez ohledu na výsledek
|
# Prevent parallel downloads overwriting heslar.csv
|
||||||
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
|
self.btn_update.setEnabled(False)
|
||||||
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
|
|
||||||
|
|
||||||
task.taskCompleted.connect(lambda: QMessageBox.information(self, "Hotovo", "Hesláře byly úspěšně aktualizovány."))
|
# Message boxes are parented to the main window, not to this dialog –
|
||||||
|
# the dialog may already be closed (and its C++ object deleted)
|
||||||
|
# by the time the minute-long task finishes.
|
||||||
|
parent_win = iface.mainWindow() if iface else None
|
||||||
|
|
||||||
# Ošetření, aby se přesně ukázala případná chyba
|
def _cleanup():
|
||||||
|
if task in _ACTIVE_TASKS:
|
||||||
|
_ACTIVE_TASKS.remove(task)
|
||||||
|
try:
|
||||||
|
self.btn_update.setEnabled(True)
|
||||||
|
except RuntimeError:
|
||||||
|
pass # dialog already closed
|
||||||
|
|
||||||
|
def on_completed():
|
||||||
|
_cleanup()
|
||||||
|
QMessageBox.information(
|
||||||
|
parent_win,
|
||||||
|
"Hotovo",
|
||||||
|
"Hesláře byly úspěšně aktualizovány."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show the exact error if the task fails
|
||||||
def on_error():
|
def on_error():
|
||||||
|
_cleanup()
|
||||||
if task.exception:
|
if task.exception:
|
||||||
# Tohle ti přesně řekne, na čem to teď padá (např. PermissionError)
|
# This will show exactly what went wrong (e.g. PermissionError)
|
||||||
msg = f"Aktualizace selhala z důvodu chyby:\n{str(task.exception)}"
|
msg = (
|
||||||
|
"Aktualizace selhala z důvodu chyby:\n"
|
||||||
|
f"{task.exception!s}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
msg = "Aktualizace byla zrušena uživatelem."
|
msg = "Aktualizace byla zrušena uživatelem."
|
||||||
QMessageBox.warning(self, "Chyba / Zrušeno", msg)
|
QMessageBox.warning(parent_win, "Chyba / Zrušeno", msg)
|
||||||
|
|
||||||
|
task.taskCompleted.connect(on_completed)
|
||||||
task.taskTerminated.connect(on_error)
|
task.taskTerminated.connect(on_error)
|
||||||
|
|
||||||
QgsApplication.taskManager().addTask(task)
|
QgsApplication.taskManager().addTask(task)
|
||||||
@@ -306,10 +657,13 @@ 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):
|
||||||
return "true" if self.chk_komponenty.isChecked() else "false"
|
if self.typ_dat in ["akce", "lokalita"]:
|
||||||
|
return "true" if self.chk_komponenty.isChecked() else "false"
|
||||||
|
return "false"
|
||||||
|
|
||||||
def get_filters(self):
|
def get_filters(self):
|
||||||
"""Compiles the user selections from the cache into API-ready filter parameters."""
|
"""Compiles the user selections from the cache into
|
||||||
|
API-ready filter parameters."""
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
if self.selection_cache['kraj']:
|
if self.selection_cache['kraj']:
|
||||||
@@ -330,28 +684,64 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.typ_dat == "akce":
|
if self.typ_dat == "akce":
|
||||||
if self.chk_posevidence.isChecked():
|
if self.chk_posevidence.isChecked():
|
||||||
filters['posevidence'] = 'true'
|
filters['posevidence'] = 'true'
|
||||||
if self.selection_cache['organizace']:
|
if self.chk_proj_akce.isChecked():
|
||||||
filters['f_organizace'] = self.selection_cache['organizace']
|
filters['proj_akce'] = 'true'
|
||||||
if self.selection_cache['typ_akce']:
|
|
||||||
filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce']
|
|
||||||
if self.selection_cache['vedouci']:
|
|
||||||
filters['f_vedouci'] = self.selection_cache['vedouci']
|
|
||||||
|
|
||||||
if self.typ_dat == "lokalita":
|
if self.selection_cache['typ_akce']:
|
||||||
if self.selection_cache['typ_lokality']:
|
filters['f_typ_vyzkumu'] = self.selection_cache['typ_akce']
|
||||||
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
|
if self.selection_cache['vedouci']:
|
||||||
if self.selection_cache['druh_lokality']:
|
filters['f_vedouci'] = self.selection_cache['vedouci']
|
||||||
filters['f_druh_lokality'] = self.selection_cache['druh_lokality']
|
|
||||||
if self.selection_cache['jistota']:
|
if self.selection_cache['organizace']:
|
||||||
filters['f_jistota'] = self.selection_cache['jistota']
|
filters['f_organizace'] = self.selection_cache['organizace']
|
||||||
if self.selection_cache['lokalita_zachovalost']:
|
|
||||||
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
|
if self.selection_cache['typ_lokality']:
|
||||||
|
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
|
||||||
|
if self.selection_cache['druh_lokality']:
|
||||||
|
filters['f_druh_lokality'] = self.selection_cache['druh_lokality']
|
||||||
|
if self.selection_cache['jistota']:
|
||||||
|
filters['f_jistota'] = self.selection_cache['jistota']
|
||||||
|
if self.selection_cache['lokalita_zachovalost']:
|
||||||
|
filters['f_lokalita_zachovalost'] = (
|
||||||
|
self.selection_cache['lokalita_zachovalost']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
class LoginDialog(QDialog):
|
class LoginDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
Dialog for saving AMČR login credentials securely in the QGIS Authentication Manager.
|
Dialog for saving AMČR login credentials securely in the
|
||||||
|
QGIS Authentication Manager.
|
||||||
|
|
||||||
Credentials are encrypted by the platform's native secret storage
|
Credentials are encrypted by the platform's native secret storage
|
||||||
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
(DPAPI on Windows, Keychain on macOS, encrypted SQLite on Linux).
|
||||||
@@ -365,10 +755,14 @@ 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
|
||||||
|
(name, method,
|
||||||
|
id) but NOT the config() values like username/password. Use full=True to
|
||||||
|
access those.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SETTINGS_KEY = "amcr_viewer/auth_config_id"
|
SETTINGS_KEY = "amcr_viewer/auth_config_id"
|
||||||
CONFIG_NAME = "AMČR Viewer"
|
CONFIG_NAME = "AMČR Viewer"
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -379,17 +773,29 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
# Check whether a config ID is already stored from a previous session.
|
# Check whether a config ID is already stored from a previous session.
|
||||||
# We attempt a lightweight load (full=False) to confirm it is readable,
|
# We attempt a lightweight load (full=False) to confirm it is readable,
|
||||||
# since hasConfigId() may return False even for valid configs (cache lag).
|
# since hasConfigId() may return False even for valid configs
|
||||||
|
# (cache lag).
|
||||||
|
# The Auth Manager must be unlocked before we attempt to read from it;
|
||||||
|
# otherwise loadAuthenticationConfig() returns ok=False even for valid
|
||||||
|
# configs, causing _has_saved to be incorrectly set to False.
|
||||||
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
self._has_saved = bool(existing_id) and bool(self._load_username_from_config(existing_id))
|
if existing_id:
|
||||||
|
QgsApplication.authManager().setMasterPassword(True)
|
||||||
|
username = self._load_username_from_config(existing_id)
|
||||||
|
self._has_saved = bool(existing_id) and bool(username)
|
||||||
|
|
||||||
if self._has_saved:
|
if self._has_saved:
|
||||||
info = QLabel("✔ Přihlašovací údaje jsou bezpečně uloženy ve správci autentizace QGIS.\n"
|
info = QLabel(
|
||||||
"Vyplňte pole níže pouze pokud je chcete změnit.")
|
"✔ Přihlašovací údaje jsou bezpečně uloženy "
|
||||||
|
"ve správci autentizace QGIS.\n"
|
||||||
|
"Vyplňte pole níže pouze pokud je chcete změnit."
|
||||||
|
)
|
||||||
info.setStyleSheet("color: green; font-style: italic;")
|
info.setStyleSheet("color: green; font-style: italic;")
|
||||||
else:
|
else:
|
||||||
info = QLabel("Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
|
info = QLabel(
|
||||||
"Budou zašifrovaně uloženy ve správci autentizace QGIS.")
|
"Zadejte přihlašovací údaje k Digitálnímu archivu AMČR.\n"
|
||||||
|
"Budou zašifrovaně uloženy ve správci autentizace QGIS."
|
||||||
|
)
|
||||||
info.setWordWrap(True)
|
info.setWordWrap(True)
|
||||||
layout.addWidget(info)
|
layout.addWidget(info)
|
||||||
layout.addSpacing(8)
|
layout.addSpacing(8)
|
||||||
@@ -406,7 +812,9 @@ class LoginDialog(QDialog):
|
|||||||
self.txt_pass = QLineEdit()
|
self.txt_pass = QLineEdit()
|
||||||
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
self.txt_pass.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.txt_pass.setPlaceholderText(
|
self.txt_pass.setPlaceholderText(
|
||||||
"ponechte prázdné pro zachování stávajícího hesla" if self._has_saved else "heslo"
|
"ponechte prázdné pro zachování stávajícího hesla"
|
||||||
|
if self._has_saved
|
||||||
|
else "heslo"
|
||||||
)
|
)
|
||||||
form.addRow("Heslo:", self.txt_pass)
|
form.addRow("Heslo:", self.txt_pass)
|
||||||
|
|
||||||
@@ -422,7 +830,8 @@ class LoginDialog(QDialog):
|
|||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self._save_and_accept)
|
buttons.accepted.connect(self._save_and_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
@@ -438,7 +847,8 @@ class LoginDialog(QDialog):
|
|||||||
def _load_config(config_id: str, full: bool = False):
|
def _load_config(config_id: str, full: bool = False):
|
||||||
"""
|
"""
|
||||||
Attempt to load a QgsAuthMethodConfig by ID.
|
Attempt to load a QgsAuthMethodConfig by ID.
|
||||||
Returns (ok, cfg). Never raises; returns (False, empty cfg) on any error.
|
Returns (ok, cfg). Never raises; returns (False, empty cfg)
|
||||||
|
on any error.
|
||||||
full=True decrypts and includes the password.
|
full=True decrypts and includes the password.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
@@ -453,8 +863,10 @@ class LoginDialog(QDialog):
|
|||||||
return False, QgsAuthMethodConfig()
|
return False, QgsAuthMethodConfig()
|
||||||
|
|
||||||
def _load_username_from_config(self, config_id: str) -> str:
|
def _load_username_from_config(self, config_id: str) -> str:
|
||||||
"""Load just the username from a stored config (no password decryption)."""
|
"""Load the username from a stored config.
|
||||||
ok, cfg = self._load_config(config_id, full=False)
|
Requires full=True since config() values are only populated
|
||||||
|
when the config is fully decrypted."""
|
||||||
|
ok, cfg = self._load_config(config_id, full=True)
|
||||||
return cfg.config("username", "") if ok else ""
|
return cfg.config("username", "") if ok else ""
|
||||||
|
|
||||||
def _ensure_master_password(self) -> bool:
|
def _ensure_master_password(self) -> bool:
|
||||||
@@ -469,16 +881,51 @@ class LoginDialog(QDialog):
|
|||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Správce autentizace nedostupný",
|
self, "Správce autentizace nedostupný",
|
||||||
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
"Správce autentizace QGIS je zakázán nebo poškozený.\n"
|
||||||
"Zkuste obnovit databázi: Nastavení → Možnosti → Autentizace → Pomůcky."
|
"Zkuste obnovit databázi: "
|
||||||
|
"Nastavení → Možnosti → Autentizace → Pomůcky."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# setMasterPassword(True) shows the QGIS master password dialog if needed
|
# setMasterPassword(True) shows the QGIS
|
||||||
|
# master password dialog if needed
|
||||||
if not auth_mgr.setMasterPassword(True):
|
if not auth_mgr.setMasterPassword(True):
|
||||||
return False # User cancelled the master password dialog
|
return False # User cancelled the master password dialog
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _verify_credentials(self, username: str, password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Verify the credentials against the API before saving them.
|
||||||
|
Returns True if they should be stored: either the login succeeded,
|
||||||
|
or the server was unreachable and the user chose to keep them
|
||||||
|
unverified. Wrong credentials are never stored.
|
||||||
|
"""
|
||||||
|
# Lazy import to avoid an import cycle
|
||||||
|
# (amcr_tools imports LoginDialog lazily as well)
|
||||||
|
from . import amcr_tools
|
||||||
|
|
||||||
|
if amcr_tools.login_to_api(username, password):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if amcr_tools.LAST_LOGIN_ERROR == 'network':
|
||||||
|
answer = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Server nedostupný",
|
||||||
|
"Přihlašovací údaje se nepodařilo ověřit – server AMČR "
|
||||||
|
"je nedostupný.\nChcete je přesto uložit (neověřené)?",
|
||||||
|
QMessageBox.StandardButton.Yes
|
||||||
|
| QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
|
return answer == QMessageBox.StandardButton.Yes
|
||||||
|
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Neplatné přihlašovací údaje",
|
||||||
|
"Přihlášení se nezdařilo – zkontrolujte e-mail a heslo.\n"
|
||||||
|
"Údaje nebyly uloženy."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Button actions
|
# Button actions
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -488,7 +935,11 @@ class LoginDialog(QDialog):
|
|||||||
password = self.txt_pass.text()
|
password = self.txt_pass.text()
|
||||||
|
|
||||||
if not username:
|
if not username:
|
||||||
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím e-mailovou adresu.")
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Chybí údaje",
|
||||||
|
"Vyplňte prosím e-mailovou adresu."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
existing_id = QSettings().value(self.SETTINGS_KEY, "")
|
||||||
@@ -501,6 +952,11 @@ class LoginDialog(QDialog):
|
|||||||
if ok:
|
if ok:
|
||||||
if not self._ensure_master_password():
|
if not self._ensure_master_password():
|
||||||
return
|
return
|
||||||
|
# Verify the new username against the stored password
|
||||||
|
if not self._verify_credentials(
|
||||||
|
username, cfg.config("password", "")
|
||||||
|
):
|
||||||
|
return
|
||||||
cfg.setConfig("username", username)
|
cfg.setConfig("username", username)
|
||||||
auth_mgr.updateAuthenticationConfig(cfg)
|
auth_mgr.updateAuthenticationConfig(cfg)
|
||||||
self.accept()
|
self.accept()
|
||||||
@@ -510,6 +966,11 @@ class LoginDialog(QDialog):
|
|||||||
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
|
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Verify before prompting for the master password – wrong
|
||||||
|
# credentials must never reach the Authentication Manager
|
||||||
|
if not self._verify_credentials(username, password):
|
||||||
|
return
|
||||||
|
|
||||||
if not self._ensure_master_password():
|
if not self._ensure_master_password():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -521,13 +982,19 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
|
|
||||||
# Try to update an existing config first; fall back to creating a new one.
|
# Try to update an existing config first;
|
||||||
# We skip hasConfigId() as it may return False despite the config existing
|
# fall back to creating a new one.
|
||||||
|
# We skip hasConfigId() as it may return False
|
||||||
|
# despite the config existing
|
||||||
# (in-memory cache may not be populated yet in QGIS 4).
|
# (in-memory cache may not be populated yet in QGIS 4).
|
||||||
ok_load, existing_cfg = self._load_config(existing_id, full=False) if existing_id else (False, None)
|
ok_load, _ = (
|
||||||
|
self._load_config(existing_id, full=False)
|
||||||
|
if existing_id
|
||||||
|
else (False, None)
|
||||||
|
)
|
||||||
if ok_load:
|
if ok_load:
|
||||||
cfg.setId(existing_id)
|
cfg.setId(existing_id)
|
||||||
ok, cfg = auth_mgr.updateAuthenticationConfig(cfg)
|
ok = auth_mgr.updateAuthenticationConfig(cfg)
|
||||||
else:
|
else:
|
||||||
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
ok, cfg = auth_mgr.storeAuthenticationConfig(cfg)
|
||||||
|
|
||||||
@@ -536,7 +1003,8 @@ class LoginDialog(QDialog):
|
|||||||
if not ok or not config_id:
|
if not ok or not config_id:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Chyba uložení",
|
self, "Chyba uložení",
|
||||||
"Přihlašovací údaje se nepodařilo uložit do správce autentizace QGIS.\n"
|
"Přihlašovací údaje se nepodařilo "
|
||||||
|
"uložit do správce autentizace QGIS.\n"
|
||||||
"Zkuste restartovat QGIS a přihlásit se znovu."
|
"Zkuste restartovat QGIS a přihlásit se znovu."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -548,9 +1016,15 @@ 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(existing_id)
|
QgsApplication.authManager().removeAuthenticationConfig(
|
||||||
|
existing_id
|
||||||
|
)
|
||||||
settings.remove(self.SETTINGS_KEY)
|
settings.remove(self.SETTINGS_KEY)
|
||||||
QMessageBox.information(self, "Hotovo", "Uložené přihlašovací údaje byly odebrány.")
|
QMessageBox.information(
|
||||||
|
self,
|
||||||
|
"Hotovo",
|
||||||
|
"Uložené přihlašovací údaje byly odebrány."
|
||||||
|
)
|
||||||
self.reject()
|
self.reject()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -565,8 +1039,9 @@ class LoginDialog(QDialog):
|
|||||||
|
|
||||||
Note: hasConfigId() is intentionally skipped here – it checks an
|
Note: hasConfigId() is intentionally skipped here – it checks an
|
||||||
in-memory cache that may lag behind the actual database contents,
|
in-memory cache that may lag behind the actual database contents,
|
||||||
causing false negatives (see class docstring). loadAuthenticationConfig()
|
causing false negatives (see class docstring).
|
||||||
is called directly and its return value is used as the authoritative result.
|
loadAuthenticationConfig() is called directly and its return value is
|
||||||
|
used as the authoritative result.
|
||||||
"""
|
"""
|
||||||
settings = QSettings()
|
settings = QSettings()
|
||||||
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
config_id = settings.value(LoginDialog.SETTINGS_KEY, "")
|
||||||
@@ -578,4 +1053,5 @@ class LoginDialog(QDialog):
|
|||||||
if not ok:
|
if not ok:
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
|
return (cfg.config("username", ""),
|
||||||
|
cfg.config("password", "")) # nosec B106
|
||||||
+980
-288
File diff suppressed because it is too large.
Load diff
+77
-28
@@ -1,15 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
|
||||||
from qgis.PyQt.QtGui import QIcon
|
|
||||||
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
|
|
||||||
from qgis.core import Qgis
|
|
||||||
from qgis.utils import iface
|
|
||||||
|
|
||||||
from .amcr_tools import load_amcr_data, login_to_api
|
|
||||||
from .amcr_dialog import AmcrFilterDialog, LoginDialog
|
|
||||||
from .resources import *
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
class AmcrViewer:
|
class AmcrViewer:
|
||||||
"""
|
"""
|
||||||
Main plugin class that manages the GUI elements, menu entries,
|
Main plugin class that manages the GUI elements, menu entries,
|
||||||
@@ -24,14 +24,17 @@ class AmcrViewer:
|
|||||||
self.iface = iface
|
self.iface = iface
|
||||||
self.plugin_dir = os.path.dirname(__file__)
|
self.plugin_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
# Determine the user's locale to load appropriate translation files
|
# Determine the user's locale to load appropriate translation files.
|
||||||
locale = QSettings().value('locale/userLocale')[0:2]
|
# The setting may be missing (None) on a fresh QGIS install.
|
||||||
|
locale = str(QSettings().value('locale/userLocale') or 'en')[0:2]
|
||||||
locale_path = os.path.join(
|
locale_path = os.path.join(
|
||||||
self.plugin_dir,
|
self.plugin_dir,
|
||||||
'i18n',
|
'i18n',
|
||||||
'AmcrViewer_{}.qm'.format(locale))
|
f'AmcrViewer_{locale}.qm'
|
||||||
|
)
|
||||||
|
|
||||||
# Install the translator if a translation file for the current locale exists
|
# Install the translator if a translation file
|
||||||
|
# for the current locale exists
|
||||||
if os.path.exists(locale_path):
|
if os.path.exists(locale_path):
|
||||||
self.translator = QTranslator()
|
self.translator = QTranslator()
|
||||||
self.translator.load(locale_path)
|
self.translator.load(locale_path)
|
||||||
@@ -39,11 +42,14 @@ class AmcrViewer:
|
|||||||
|
|
||||||
# Initialize internal state
|
# Initialize internal state
|
||||||
self.actions = []
|
self.actions = []
|
||||||
self.menu = self.tr(u'&AMČR Viewer')
|
self.menu = self.tr('&AMČR Viewer')
|
||||||
self.first_start = None
|
self.first_start = None
|
||||||
|
|
||||||
def tr(self, message):
|
def tr(self, message):
|
||||||
"""Helper method for translating strings within the AmcrViewer context."""
|
"""
|
||||||
|
Helper method for translating strings within
|
||||||
|
the AmcrViewer context.
|
||||||
|
"""
|
||||||
return QCoreApplication.translate('AmcrViewer', message)
|
return QCoreApplication.translate('AmcrViewer', message)
|
||||||
|
|
||||||
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
def add_action(self, icon_path, text, callback, enabled_flag=True,
|
||||||
@@ -71,7 +77,8 @@ class AmcrViewer:
|
|||||||
if add_to_menu:
|
if add_to_menu:
|
||||||
self.iface.addPluginToMenu(self.menu, action)
|
self.iface.addPluginToMenu(self.menu, action)
|
||||||
|
|
||||||
# Store only actions that are directly attached to the QGIS UI for later cleanup
|
# Store only actions that are directly attached
|
||||||
|
# to the QGIS UI for later cleanup
|
||||||
if add_to_toolbar or add_to_menu:
|
if add_to_toolbar or add_to_menu:
|
||||||
self.actions.append(action)
|
self.actions.append(action)
|
||||||
|
|
||||||
@@ -84,16 +91,19 @@ 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_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')
|
||||||
|
|
||||||
# 1. Create a container menu for the plugin
|
# 1. Create a container menu for the plugin
|
||||||
self.plugin_menu = QMenu()
|
self.plugin_menu = QMenu()
|
||||||
|
|
||||||
# 2. Create sub-actions (Download Projects / Download Sites)
|
# 2. Create sub-actions (Download Projects / Download Sites)
|
||||||
# add_to_menu/toolbar is False because these go into our custom dropdown menu
|
# add_to_menu/toolbar is False because these go into our
|
||||||
|
# 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(u'Stáhnout data akcí | AMČR Viewer'),
|
text=self.tr('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,
|
||||||
@@ -101,9 +111,20 @@ class AmcrViewer:
|
|||||||
)
|
)
|
||||||
self.plugin_menu.addAction(self.action_download_akce)
|
self.plugin_menu.addAction(self.action_download_akce)
|
||||||
|
|
||||||
|
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'),
|
||||||
|
parent=self.iface.mainWindow(),
|
||||||
|
add_to_menu=False,
|
||||||
|
add_to_toolbar=False
|
||||||
|
)
|
||||||
|
self.plugin_menu.addAction(self.action_download_pas)
|
||||||
|
|
||||||
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(u'Stáhnout data lokalit | AMČR Viewer'),
|
text=self.tr('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,
|
||||||
@@ -112,8 +133,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_akce_path,
|
icon_path=icon_pas_path,
|
||||||
text=self.tr(u'Přihlásit se | AMČR Viewer'),
|
text=self.tr('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,
|
||||||
@@ -121,9 +142,23 @@ class AmcrViewer:
|
|||||||
)
|
)
|
||||||
self.plugin_menu.addAction(self.action_login_dialog)
|
self.plugin_menu.addAction(self.action_login_dialog)
|
||||||
|
|
||||||
|
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'),
|
||||||
|
callback=lambda checked=False: self.open_help(),
|
||||||
|
parent=self.iface.mainWindow(),
|
||||||
|
add_to_menu=False,
|
||||||
|
add_to_toolbar=False
|
||||||
|
)
|
||||||
|
self.plugin_menu.addAction(self.action_amcr_help)
|
||||||
|
|
||||||
# 3. Create the main project action and attach the menu to it
|
# 3. Create the main project action and attach the menu to it
|
||||||
main_icon = QIcon(icon_akce_path)
|
main_icon = QIcon(icon_akce_path)
|
||||||
self.main_action = QAction(main_icon, 'AMČR Viewer', self.iface.mainWindow())
|
self.main_action = QAction(
|
||||||
|
main_icon,
|
||||||
|
'AMČR Viewer',
|
||||||
|
self.iface.mainWindow()
|
||||||
|
)
|
||||||
self.main_action.setMenu(self.plugin_menu)
|
self.main_action.setMenu(self.plugin_menu)
|
||||||
self.iface.addPluginToMenu(self.menu, self.main_action)
|
self.iface.addPluginToMenu(self.menu, self.main_action)
|
||||||
|
|
||||||
@@ -132,9 +167,12 @@ class AmcrViewer:
|
|||||||
self.tool_button = QToolButton()
|
self.tool_button = QToolButton()
|
||||||
self.tool_button.setMenu(self.plugin_menu)
|
self.tool_button.setMenu(self.plugin_menu)
|
||||||
self.tool_button.setDefaultAction(self.action_download_akce)
|
self.tool_button.setDefaultAction(self.action_download_akce)
|
||||||
self.tool_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
|
self.tool_button.setPopupMode(
|
||||||
|
QToolButton.ToolButtonPopupMode.MenuButtonPopup
|
||||||
|
)
|
||||||
|
|
||||||
# Add the widget directly to the toolbar and store the reference for cleanup
|
# Add the widget directly to the toolbar
|
||||||
|
# and store the reference for cleanup
|
||||||
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
self.toolbar_action = self.iface.addToolBarWidget(self.tool_button)
|
||||||
|
|
||||||
self.first_start = True
|
self.first_start = True
|
||||||
@@ -172,13 +210,15 @@ class AmcrViewer:
|
|||||||
dlg = AmcrFilterDialog(typ_dat)
|
dlg = AmcrFilterDialog(typ_dat)
|
||||||
result = dlg.exec()
|
result = dlg.exec()
|
||||||
|
|
||||||
# If user confirmed the dialog (OK button), gather filters and load data
|
# If user confirmed the dialog (OK button),
|
||||||
|
# gather filters and load data
|
||||||
if result == QDialog.DialogCode.Accepted:
|
if result == QDialog.DialogCode.Accepted:
|
||||||
filters = dlg.get_filters()
|
filters = dlg.get_filters()
|
||||||
bbox = dlg.get_bbox()
|
bbox = dlg.get_bbox()
|
||||||
komponenty = dlg.get_komponenty()
|
komponenty = dlg.get_komponenty()
|
||||||
|
|
||||||
# Access the map canvas and start the fetch/render process from amcr_tools
|
# Access the map canvas and start
|
||||||
|
# the fetch/render process from amcr_tools
|
||||||
canvas = self.iface.mapCanvas()
|
canvas = self.iface.mapCanvas()
|
||||||
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
load_amcr_data(canvas, bbox, filters, typ_dat, komponenty)
|
||||||
|
|
||||||
@@ -190,9 +230,18 @@ class AmcrViewer:
|
|||||||
session = login_to_api(username, password)
|
session = login_to_api(username, password)
|
||||||
if session:
|
if session:
|
||||||
self.iface.messageBar().pushMessage(
|
self.iface.messageBar().pushMessage(
|
||||||
"AMČR", "Přihlášení proběhlo úspěšně.", level=Qgis.MessageLevel.Success
|
"AMČR",
|
||||||
|
"Přihlášení proběhlo úspěšně.",
|
||||||
|
level=Qgis.MessageLevel.Success
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.iface.messageBar().pushMessage(
|
self.iface.messageBar().pushMessage(
|
||||||
"AMČR", "Přihlášení se nezdařilo – viz záložka AMČR login v panelu Zprávy.", level=Qgis.MessageLevel.Critical
|
"AMČR",
|
||||||
|
"Přihlášení se nezdařilo – viz záložka AMČR login "
|
||||||
|
"v panelu Zprávy.",
|
||||||
|
level=Qgis.MessageLevel.Critical
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def open_help(self):
|
||||||
|
help_url = "https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html"
|
||||||
|
QDesktopServices.openUrl(QUrl(help_url))
|
||||||
+3230
-8240
File diff suppressed because it is too large.
Load diff
@@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
[general]
|
[general]
|
||||||
name=AMČR Viewer
|
name=AMČR Viewer
|
||||||
qgisMinimumVersion=3.4.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.0.0-alpha.2
|
version=2.1.2
|
||||||
author=David Spáčil
|
author=David Spáčil
|
||||||
email=spacil@arub.cz
|
email=spacil@arub.cz
|
||||||
|
|
||||||
@@ -22,16 +22,46 @@ 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
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
* Fixed QGIS minimum version
|
||||||
|
v2.0.0 (2026-06-05)
|
||||||
|
* Added warning regarding the feature duplication when loading components to the dialog
|
||||||
|
* Added Help button to the plugin menu
|
||||||
|
* Code clean-up
|
||||||
|
v2.0.0-alpha.4 (2026-06-03)
|
||||||
|
* Backend filtering of the results based on the component-related filters improvement (plugin not only loads the results from API, it filters them further)
|
||||||
|
v2.0.0-alpha.2–3 (2026-05-19)
|
||||||
|
* Security vulnerabilities fix
|
||||||
|
v2.0.0-alpha.1 (2026-05-19):
|
||||||
|
* Attribute fields renamed to be ASCII compliant
|
||||||
|
* Codelist update; codelist can be recompiled from AMČR API
|
||||||
|
* Base element changed from Documentation Unit to Component if user asks for components to simplify result filtering
|
||||||
|
* Plugin now supports logging with an AMČR account and enables the downloading of Events and Sites available to the roles Researcher and higher
|
||||||
|
|
||||||
# Tags are comma separated with spaces allowed
|
# Tags are comma separated with spaces allowed
|
||||||
tags=python,AMCR,AIS CR,archaeology,PIAN,AMČR
|
tags=python,AMCR,AIS CR,archaeology,PIAN,AMČR,archeologie
|
||||||
|
|
||||||
homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
|
homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
|
||||||
category=Vector
|
category=Vector
|
||||||
icon=download.png
|
icon=akce.png
|
||||||
# experimental flag
|
# experimental flag
|
||||||
experimental=True
|
experimental=False
|
||||||
|
|
||||||
# deprecated flag (applies to the whole plugin, not just a single version)
|
# deprecated flag (applies to the whole plugin, not just a single version)
|
||||||
deprecated=False
|
deprecated=False
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 871 B |
@@ -0,0 +1,44 @@
|
|||||||
|
# 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í.
|
||||||
|
#
|
||||||
|
# Konfigurace flake8 je záměrně jinde: v amcr_viewer/.flake8, protože ji
|
||||||
|
# musí najít i scanner na plugins.qgis.org.
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 79
|
||||||
|
# QGIS 3.44 běží na Pythonu 3.9 a novějším
|
||||||
|
target-version = "py39"
|
||||||
|
# Generovaný výstup pyrcc, "All changes made in this file will be lost"
|
||||||
|
extend-exclude = ["amcr_viewer/resources.py"]
|
||||||
|
|
||||||
|
[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",
|
||||||
|
]
|
||||||
+23
-7
@@ -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.3 (0d15f75, 2025-12-25)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
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="6.8059028"
|
inkscape:cx="0.57452426"
|
||||||
inkscape:cy="14.606174"
|
inkscape:cy="15.556349"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="1009"
|
inkscape:window-height="1131"
|
||||||
inkscape:window-x="1912"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="-8"
|
inkscape:window-y="0"
|
||||||
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,4 +96,20 @@
|
|||||||
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" /></g></svg>
|
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>
|
||||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,85 @@
|
|||||||
|
# -*- 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")
|
||||||
|
|
||||||
|
# Files that belong in the plugin package even though the upload scanner
|
||||||
|
# would otherwise call them hidden
|
||||||
|
POVOLENE_SKRYTE = {".flake8", ".bandit", ".secrets.baseline"}
|
||||||
|
|
||||||
|
# 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)})")
|
||||||
|
if jmeno.startswith(".") and jmeno not in POVOLENE_SKRYTE:
|
||||||
|
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ů")
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- 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")
|
||||||
Reference in new issue
Block a user