Compare commits

...
37 Commits
Author SHA1 Message Date
david-spacil 70aba60d12 Merge pull request #62 from ARUP-CAS/agents/claude/ci-kontroly
ci: kontroly kvality kódu při každém PR
2026-09-01 23:49:50 +02:00
david-spacilandClaude Opus 5 3591ac4c53 ci: kontrola kvality kódu při každém PR
Workflow code_quality.yml pouští při každém PR a při pushi do main
kontroly, které se dosud dělaly ručně, ve čtyřech jobech:

- Lint a bezpečnost: check_sources.py, bandit, detect-secrets, flake8,
  ruff. Bandit a detect-secrets jsou tytéž kontroly, které blokují
  schválení na plugins.qgis.org.
- Kompatibilita s Qt6: pyqgis4-checker v dockeru. Skript končí kódem 0,
  i když něco najde, výsledek je jen v logu; job proto ověřuje, že log
  obsahuje pouze hlavičku.
- Smoke test: smoke_test.py v qgis/qgis:ltr (3.44, Qt5) i
  qgis/qgis:stable (4.x, Qt6). Tagy se posouvají schválně, aby bylo
  vidět, že plugin drží krok s aktuálním QGISem.
- Balíček pluginu: sestaví amcr_viewer.zip stejně jako release
  workflow, ověří, že v něm je metadata.txt, __init__.py i .flake8
  a nejsou v něm git soubory, a přiloží ho jako artefakt běhu.
  Recenzent ho nainstaluje přes Install from ZIP bez ručního balení.

detect-secrets se pouští s --all-files. Bez toho prohledá jen soubory
sledované gitem a o nesledovaném souboru mlčí, což vypadá jako čistý
výsledek.

Verze nástrojů jsou napevno. Bez pinu by se výsledek měnil s každým
vydáním ruffu, které rozšíří výchozí sadu pravidel.

CodeQL a GitGuardian běží zvlášť, nastavené na úrovni organizace,
a schválně se tu neduplikují.

AGENTS.md popisuje, jak totéž pustit lokálně, a tři místa, kde kontroly
tiše lžou (výstupní kód pyqgis4-checkeru, detect-secrets bez
--all-files, umístění config souborů pro scanner).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhQyc1uQFcpkezLwM8if6T
2026-09-01 23:43:53 +02:00
david-spacilandClaude Opus 5 f4b1c39e6c test: kontrola zdrojáků a smoke test v QGIS
tests/check_sources.py hlídá pravidla, která nepotřebují QGIS a jejichž
porušení je tiché:

- UTF-8 BOM na začátku .py souboru. Pythonu ani QGISu nevadí, ale
  oficiální pyqgis4-checker na něm spadne a soubor vůbec nezkontroluje,
  takže rozbitý soubor vypadá čistě. Stalo se tady už dvakrát.
- přímý import z PyQt5/PyQt6 mimo shim qgis.PyQt
- spustitelná práva, skryté soubory a podezřelé typy souborů, které
  hlásí analýza souborů na plugins.qgis.org

tests/smoke_test.py načte plugin ve skutečném QGIS, přečte scoped
enumy, vytvoří UpdateCodelistsTask a všechny tři filtrační dialogy
a ověří doplňování hraničního data. Běží offline a bez X serveru
(QT_QPA_PLATFORM=offscreen), takže nezávisí na dostupnosti API.

Očekávaná hodnota filtru je v testu napsaná natvrdo, ne přes
DATE_OPEN_TO. Porovnání proti konstantě z modulu dokazuje jen to, že se
modul shodne sám se sebou, a prošlo i s rozbitým sentinelem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhQyc1uQFcpkezLwM8if6T
2026-09-01 23:43:33 +02:00
david-spacilandClaude Opus 5 bcb0bc2c86 chore: konfigurace lintů
Bez explicitní konfigurace se výsledek lintů mění sám od sebe: výchozí
sada pravidel ruffu se liší verzi od verze a flake8 hlásí generovaný
resources.py, který se ručně neformátuje.

Konfigurace 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; stejná pravidla tak platí v CI,
lokálně i při uploadu. Konfigurace ruffu je v kořenovém pyproject.toml,
ruff se do balíčku pluginu nedistribuuje.

Ignorovaná pravidla mají v pyproject.toml odůvodnění: UP009 (hlavička
utf-8 je konvence šablony Plugin Builderu), BLE001 (except Exception je
záměr, výjimka nesmí propadnout do QGISu), SIM103 a SIM105 (čitelnost).

Dvě opravy, které z konfigurace plynou:
- open(path, 'r', encoding=...) -> open(path, encoding=...)  [UP015]
- zbytečné else: po return v get_komponenty()                [RET505]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhQyc1uQFcpkezLwM8if6T
2026-09-01 23:43:20 +02:00
david-spacil 39dac12bad Merge pull request #61 from ARUP-CAS/fix/qt6-kompatibilita
fix: kompatibilita s Qt6 / QGIS 4 a úklid lintů
2026-09-01 23:22:56 +02:00
david-spacil b708930ca2 update metadata 2026-09-01 23:21:04 +02:00
david-spacil 3f7839c818 fix: znovu odstraněn BOM a zalomen dlouhý import
Commit 9aba283 vrátil UTF-8 BOM do amcr_dialog.py a amcr_tools.py –
patrně ho doplnil editor při uložení. Oficiální pyqgis4-checker na nich
kvůli tomu znovu padal na SyntaxError, tedy přesně to, co d398d2c
opravoval.

Zároveň zalomen sloučený import z qgis.core v amcr_dialog.py, který měl
87 znaků.
2026-09-01 23:15:28 +02:00
david-spacil 9aba28317a chore: pořadí importů 2026-09-01 23:13:18 +02:00
david-spacil 048ffe4e2a chore: úklid nálezů z lintů
Bez změny chování. Kontroly, které pouští plugins.qgis.org (flake8),
plus ruff.

flake8: odstraněny koncové bílé znaky, srovnány nadbytečné prázdné řádky
a zalomeno 12 řádků přes 79 znaků. Hlášku o HTTP chybě v login_to_api()
si nešlo jen zalomit – tělo odpovědi je nově v proměnné `telo`, výsledný
text je stejný.

ruff: odstraněny zbytečné prefixy u'' u překládaných řetězců (6×),
.format() nahrazen f-stringem, str() uvnitř f-stringu převedeno na !s,
`k in dict.keys()` na `k in dict`, prázdný komentář; nepoužitá rozbalená
proměnná `existing_cfg` nahrazena `_`.

Vědomě neřešeno:
- resources.py (5 zbylých nálezů flake8) je generovaný výstup pyrcc,
  nese hlavičku "All changes made in this file will be lost", není nikde
  importovaný a v repu k němu chybí zdrojový .qrc.
- ruff BLE001 (10×) hlásí `except Exception`. Ty jsou v pluginu záměrné:
  drží pád mimo QGIS a chybu ukážou v liště zpráv. Změna by byla zásah
  do chování, ne úklid.
- ruff SIM103 (2×) navrhuje sloučit strážní podmínky do jednoho výrazu.
  Obě místa mají ke každé větvi vysvětlující komentář, sloučením by se
  staly hůř čitelnými.
- ruff UP009 a I001 (coding hlavička a řazení importů) jsou celoprojektová
  konvence; jejich změna patří do samostatného rozhodnutí, ne sem.

Ověřeno v QGIS 3.44.13 (Qt5) i QGIS 4.2.1 (Qt6): importy, vytvoření tasku
i všechny tři filtrační dialogy fungují shodně. Bandit, detect-secrets
i pyqgis4-checker hlásí nula nálezů.
2026-09-01 23:10:33 +02:00
david-spacil d398d2cd1b fix: kompatibilita s Qt6 / QGIS 4
Oficiální kontrola pyqgis4-checker (skript pyqt5_to_pyqt6.py z QGISu)
hlásila u pluginu 13 nekompatibilit a tři soubory vůbec nepřečetla.

Odstraněn UTF-8 BOM z amcr_codelists.py, amcr_dialog.py a amcr_tools.py.
Checker čte zdroják jako UTF-8 bez utf-8-sig a na BOM padá na
SyntaxError: invalid non-printable character U+FEFF, takže se ty soubory
nezkontrolovaly vůbec. Pythonu při běhu BOM nevadí, proto to nikdy
nevyskočilo. heslar.csv si BOM ponechává, tam je kvůli Excelu.

Enumy převedeny na plně kvalifikované tvary (13 míst):
Qgis.{Info,Warning,Critical} -> Qgis.MessageLevel.*,
QgsTask.CanCancel -> QgsTask.Flag.CanCancel,
QgsWkbTypes.*Geometry -> QgsWkbTypes.GeometryType.*Geometry.
Zkrácené tvary v QGIS 4.2.1 zatím fungují, ale kontrola je vytýká
a aliasy do budoucna mizí. Zbytek kódu už scoped tvary používal.

Do AGENTS.md doplněna sekce o kompatibilitě s Qt6 / QGIS 4 se závaznými
pravidly a příkazy na ověření.

Ověřeno spuštěním v QGIS 3.44.13 (Qt 5.15.18) i QGIS 4.2.1 (Qt 6.10.3):
importy, vytvoření tasku i všechny tři filtrační dialogy fungují shodně.
Log pyqgis4-checkeru je nově prázdný.
2026-09-01 23:05:02 +02:00
david-spacil 2218719f98 metadata update 2026-09-01 22:31:28 +02:00
david-spacil 6300919c36 update workflow kvůli nové podmínce s immutable releases 2026-09-01 22:27:00 +02:00
david-spacil 7cffcdb235 Merge pull request #60 from ARUP-CAS/version/v2.1.0
release: AMČR Viewer 2.1.0
2026-09-01 22:11:36 +02:00
david-spacil 0af4a0cd92 chore: verze 2.1.0 a changelog
Shrnuje změny sloučené do této větve oproti main: projektové akce (#53), úklid kódu (#54), stahování samostatných nálezů (#57), diagnostiku prázdných výsledků z API (#58) a filtrování podle data se scrollovatelným dialogem (#59).
2026-09-01 21:59:34 +02:00
david-spacil 9b25863031 chore: update přibaleného hesláře 2026-09-01 21:58:08 +02:00
david-spacil 9ec71c7ed3 feat: filtrování záznamů podle data (#59)
* fix: scrollovatelný filtrační dialog

Obsah dialogu přesahoval okno u všech tří typů dat – u akcí zabíral
přes 1280 px v okně vysokém 750 px, takže spodní filtry i tlačítka
byly nedostupné.

Filtry se nově scrollují. Tlačítka OK / Aktualizovat hesláře / Cancel
zůstávají mimo scrollovanou oblast, aby se kvůli potvrzení dialogu
nemuselo scrollovat na konec seznamu.

* feat: filtrování záznamů podle data

Přidává filtr na datum zahájení a ukončení u akcí a na datum nálezu
u samostatných nálezů (#56). Lokalita filtr nedostává – v jejím indexu
žádné datumové pole není, takže by šlo o ovládací prvek bez účinku.

API vyžaduje obě meze intervalu: jednostranný rozsah shodí server na
ArrayIndexOutOfBoundsException a hvězdičku nepřijímá. Prázdné pole se
proto nahrazuje zarážkou 0001-01-01 / 9999-12-31, což jsou shodou
okolností i výchozí meze QgsDateEdit. Blok s oběma poli prázdnými
neposílá do dotazu nic.

Obrácené rozmezí vrací API jako nula záznamů bez chybové hlášky, což
je k nerozeznání od skutečně prázdného výsledku – dialog ho proto
odmítne už při potvrzení a vypíše, kterých filtrů se to týká.
2026-09-01 21:50:55 +02:00
david-spacil 4ed99d73d9 fix: diagnostika prázdných výsledků z API (#58)
- logování odeslané URL a chyb z těla odpovědi v _api_get_json
- výpis filtrů z dialogu před zahájením stahování
- rozlišení chyby API od prázdného výsledku (api_error)
- dávky geometrií PIAN hlásí chybějící blok 'response'

API vrací chyby s HTTP 200 a klíčem 'error' místo bloku 'response'; resp_json.get('response', {}) z toho udělal prázdný výsledek, takže neplatný parametr, prázdné okno mapy i chyba serveru končily stejnou hláškou "Žádné záznamy nenalezeny".
2026-09-01 20:39:55 +02:00
david-spacil 4699dd9c95 feat: stahování samostatných nálezů (PAS) (#57)
* feature: dialog: přidána nová pole
přidána pole samostatných nálezů pro filtrovací dialog + upravena viditelnost některých spolčných polí (např. skrýt areál, zobrazit organizaci nejen pro akce, ale i pro PAS, ...)

* feature: tlačítko v menu
v kontextové nabídce přibylo tlačítko pro filtrování PASových záznamů; ikona je zatím placeholder

* feature: rozšíření slovníčku
slovníček interních vs api klíčů byl rozšířen o nová pole (nálezce, okolnosti, ...) a zároveň byla změněna jeho struktura: nově obsahuje i base url vzhledem k tomu, že si skript pro data sahá do dvou různých API; přidáno base url pro digiarchiv

* feature: update funkcí pro stahování dat
funkce fetch_set a download_heslare byly upraveny pro stahování dat nejen z OAI-PMH API, ale nově i z API digiarchivu (= osoby se nově nestahují z hesláře osob, ale z facetek digiarchivu, kde mají osoby přidělené "role" nálezce/vedoucí)

* feature: globals pro PAS
založeny nové heslářové globals, pak přidány do funkce refresh_globals

* feature: nové heslářové globals + cache v dialogu

* feature: aktualizace hesláře

* fix: oprava typ_dat = "pas" na typ_dat = "samostatny_nalez"

* feature: dynamičtjší způsob interpretace typu dat
- archeologicky_zaznam nově čerpá human-readable název pro název vrstvy v typ_dat_vocab
- archeologicky_zaznam_l pro ověřování, jestli je současný typ_dat akce nebo lokalita

* feature (wip): první krůčky logiky pro parsování PAS záznamů
- dosavadní logika parsování platná pro akce a lokality je podmíněna typ_dat
- stub logiky parsování pro samostatné nálezy

* feature: čtení dat z JSON payload
získávání sn-specific dat, jako je nálezce, hloubka nálezu, ale i vlastní wkt, které není závislé na PIANu

* feature: párovací slovník metadat pro samostatné nálezy

* fix: Přístupnost přidána do hesláře aliasů

* fix: oprava chyb z dialogu znemožňujících stahování SN

* fix: předávání filtrů k samostatným nálezům z dialogu do "tools" skriptu

* feat: drobné změny
- escapování názvů vrstev (podtržítko místo mezery)
- `actions_with_geom` -> `entries_with_geom`
- čitelný `typ_dat` pro PAS: `PAS` -> `Samostatný nález`

* feat: dokončení smyčky na ukládání metadat k SN z docs

* feat: plnění vrstvy daty SN

* feat: update ikon pro samostatné nálezy a login
2026-09-01 20:28:31 +02:00
david-spacil 7ea2a99ada fix/small-fixes: drobné úpravy stylu a čitelnosti kódu (#54)
* small fixes

* fix: refactoring
rozbaleny listy, doplněny trailing commas
2026-06-28 11:03:15 +02:00
david-spacil 6772a99ead feature/projektové akce: filtrování a sloupec Projekt v akcích (#53)
* feature/projektove-akce: do dialogu akce přidán checkbox pro filtrování projektových akcí + backend logika

* feature/projektove-akce: do atributové tabulky akcí přidáno pole Projekt (prázdné, pokud nejde o projektovou akci)
2026-06-28 10:35:25 +02:00
Claude 9e8863b879 chore: sladění governance se vzorem aiscr-management
- ekosystémová poznámka (sibling hubu aiscr-management)
- konvence větví feat/fix/docs/chore + agents/<jméno>/<téma>
- pravidla pro AI agenty (git delivery)
- bezpečnost/soukromí (žádné secrets/PII)
- deterministické datum pro changelog
- PR šablona: pole Zapojení AI + položka konvence větví

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8nJvecSSf6vB3NYZsMaVt
2026-06-28 10:26:06 +02:00
Claude 0eb8008e27 chore: PR šablona + AGENTS.md (a CLAUDE.md odkaz) pro standardizaci repozitáře
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C8nJvecSSf6vB3NYZsMaVt
2026-06-28 10:24:00 +02:00
david-spacil bc799452a3 Update metadata.txt 2026-06-13 12:16:27 +02:00
david-spacil ee558aa718 Merge pull request #49 from ARUP-CAS/agents/claude/fix-optim
Opravy chyb a optimalizace napříč pluginem

Souhrn revize celého kódu pluginu:

- Opravy pádů: chybějící locale při startu QGIS; životní cyklus
  úlohy aktualizace heslářů (GC reference, zákaz souběhu, message
  boxy nad hlavním oknem).
- Datová korektnost: transformace fallback geometrií WGS-84 do
  S-JTSK, oprava nevalidních polygonů přes makeValid(), normalizace
  hodnot pian_presnost/pian_typ, heslář přístupnosti bez záznamů
  s kódem None, ošetření null hodnot u komponent.
- Přihlašování a stahování: ověření údajů před uložením do správce
  autentizace, zřetelné hlášení síťových chyb a neúplných výsledků,
  jedno parsování JSON odpovědí, úspornější stránkování, guard proti
  souběžným stahováním, logování do QgsMessageLog.
- Metadata a CI: oprava ikony pluginu, akce ve workflow povýšeny na
  aktuální verze a připnuty na commit hash, poznámka o závislosti
  na requests v README.

Otestováno ručně v QGIS dle checklistu v PR (včetně hraničních
případů: výpadek sítě, expirovaná session, špatné heslo, zrušení
úlohy, prázdné výsledky, limit záznamů).
2026-06-12 13:59:46 +02:00
Claude eebd7668a5 Workflow: připnutí GitHub Actions na commit hash + aktualizace verzí
Reakce na CodeQL upozornění 'Unpinned tag for a non-immutable Action'.
Tag (v4/v2) může vlastník akce kdykoli přepsat na jiný commit; připnutí
na hash chrání release pipeline před podvržením závislosti.

Zároveň povýšeno na nejnovější verze: actions/checkout v6.0.3
a softprops/action-gh-release v3.0.0 (jen přechod na Node 24 runtime,
bez změn API; hashe ověřeny přes git ls-remote).
2026-06-12 13:46:47 +02:00
Claude fd11bee274 Ověření přihlašovacích údajů před uložením do správce autentizace
Špatné heslo zadané v přihlašovacím dialogu se dosud uložilo do QGIS
Authentication Manageru a chyba se ukázala až po zavření dialogu.

- Dialog nyní před uložením zkusí přihlášení k API; při neplatných
  údajích se nic neuloží a uživatel může údaje rovnou opravit.
- Pokud je server nedostupný (údaje nelze ověřit), uživatel si může
  zvolit, zda je uložit neověřené – změna hesla na webu u dříve
  uložených údajů tím není dotčena.
- login_to_api nově rozlišuje důvod selhání (LAST_LOGIN_ERROR:
  'auth' × 'network'), aby dialog uměl oba případy odlišit.
- Ověřuje se i změna samotného e-mailu (proti uloženému heslu).
2026-06-12 12:53:28 +02:00
Claude 93ed0ca810 Zřetelné hlášení síťových chyb během stahování
Při výpadku sítě uprostřed stahování se dosud chyba jen zapsala do logu:
stránkování se tiše ukončilo s částečnými daty a smyčka PIAN zkoušela
marně všechny zbývající dávky, takže uživatel skončil u hlášky 'Žádná
data k zobrazení' bez vysvětlení.

- Síťové chyby (requests.RequestException) se v obou smyčkách chytají
  zvlášť a stahování PIAN se po první z nich ihned ukončí.
- Pokud selže už stahování metadat, zobrazí se červená lišta
  'Stahování selhalo: chyba sítě'.
- Pokud se stihla vykreslit část dat, zobrazí se varování, že výsledek
  je neúplný a je třeba stahování zopakovat.
2026-06-12 12:35:51 +02:00
Claude 64ec1ea7fd Metadata, CI a dokumentace
- metadata.txt: icon ukazoval na neexistující download.png – plugin se
  ve správci zásuvných modulů zobrazoval bez ikony; nastaveno na akce.png.
- Workflow: actions/checkout v2→v4 (v2 je deprecated, Node 12)
  a softprops/action-gh-release v1→v2.
- README: poznámka, že knihovna requests nemusí být na Linuxu součástí
  systémové instalace QGIS.
2026-06-12 10:50:04 +02:00
Claude 27e5fe02ac Heslář pristupnost: přeskočení záznamů bez platného kódu
Pokud žádný titulek záznamu neprojde filtrem na jednopísmenný kód,
next() vrátil None a do heslar.csv se zapsal prázdný/None kód, který
se mohl dostat do API filtru jako hodnota 'None:or'. Takové záznamy
se nyní přeskakují.
2026-06-12 10:50:04 +02:00
Claude 46c09c4a09 Robustnější síťová vrstva a stahování dat
- login_to_api: ošetřena výjimka při parsování ne-JSON odpovědi
  (např. HTML chybová stránka za proxy) – dříve propadla až do QGIS.
- _api_get přejmenováno na _api_get_json: tělo odpovědi se parsuje jen
  jednou (kontrola expirované session ho znovu využije), místo dvojího
  json() na každé dávce.
- Stránkování porovnává počet stažených (ne unikátních) záznamů proti
  numFound – stránky plné duplicit už nevyvolávají zbytečné requesty.
- Přidán re-entrancy guard: během běžícího stahování (processEvents
  pumpuje event loop) nelze spustit druhé stahování.
- print() nahrazeno QgsMessageLog (záložka AMČR v panelu Zprávy).
2026-06-12 10:50:03 +02:00
Claude 444d1c4826 Oprava geometrií a filtrování PIAN
- Fallback geometrie z geom_wkt (WGS-84) se nyní transformuje do S-JTSK;
  dříve se souřadnice ve stupních vkládaly přímo do vrstvy EPSG:5514
  a prvek se vykreslil úplně mimo.
- Nevalidní geometrie (např. samoprůniky polygonů) se před zahozením
  zkusí opravit přes makeValid().
- Filtr přesnosti PIAN normalizuje hodnotu pian_presnost (API ji může
  vracet jako jednoprvkový seznam) – dříve mohl tiše zahazovat záznamy.
  Stejná normalizace pro pian_typ kvůli překladu kódu.
- Přístupy ke komponenta_areal/komponenta_obdobi a az_chranene_udaje
  ošetřeny proti hodnotě None (klíč existuje, ale je null).
2026-06-12 10:50:03 +02:00
Claude d9f5d2ae6e Oprava pádů: chybějící locale při startu a životní cyklus úlohy aktualizace heslářů
- amcr_viewer.py: QSettings().value('locale/userLocale') může být None na
  čisté instalaci QGIS – ošetřen fallback na 'en', jinak plugin spadne
  na TypeError hned při načtení.
- amcr_viewer.py: odstraněn nepoužívaný hvězdičkový import resources.
- amcr_dialog.py: úloha aktualizace heslářů se drží v modulové referenci,
  aby GC neuklidil Python wrapper QgsTask před dokončením (známá příčina
  pádů QGIS).
- amcr_dialog.py: tlačítko aktualizace se po spuštění zakáže – nelze už
  spustit několik stahování paralelně přepisujících heslar.csv.
- amcr_dialog.py: závěrečné QMessageBoxy se parentují na hlavní okno QGIS
  místo dialogu, který může být v době dokončení úlohy už zavřený
  (smazaný C++ objekt → pád).
2026-06-12 10:50:03 +02:00
david-spacil d417a78b85 Update metadata.txt 2026-06-10 13:40:14 +02:00
david-spacil ca827321d8 Update README.md 2026-06-10 13:33:33 +02:00
david-spacil b2001c625c Hotfix: oprava minimální podporované QGIS verze (3.4→3.44) (#48) 2026-06-10 13:31:55 +02:00
david-spacil 88018aa432 Update README.md 2026-06-10 13:16:33 +02:00
david-spacil 45b6ab09b2 Update CITATION.cff 2026-06-05 14:12:54 +02:00
20 changed files with 5086 additions and 8456 deletions

No files matched your search

+35
View File
@@ -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. -->
+171
View File
@@ -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
+22 -8
View File
@@ -1,17 +1,28 @@
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:
release:
types: [published]
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
# 1. Stáhne kód z repozitáře
# 1. Stáhne kód z tagu, který běh spustil
- 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')
- name: Zip Plugin
@@ -20,11 +31,14 @@ jobs:
# -r = rekurzivně, -x = ignorovat skryté git soubory
zip -r amcr_viewer.zip amcr_viewer -x "*.git*"
# 3. Nahraje ZIP k Releasu
- name: Upload Release Asset
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
# 3. Založí koncept releasu i s přílohou
# Tagy s pomlčkou (v2.0.0-alpha.1) se označí jako pre-release.
- name: Create draft release with asset
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
files: amcr_viewer.zip
draft: true
name: AMCR Viewer ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+232
View File
@@ -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
View File
@@ -20,11 +20,10 @@ identifiers:
value: 10.5281/zenodo.18609813
repository-code: 'https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer'
abstract: >-
This QGIS plugin is intended for downloading the data
(Fieldwork events data only, at the time) from the
Digiarchive of the Archaeological Map of the Czech
Republic (AMCR). As of now, only publicly accessible data
can be downloaded.
This plugin is intended for downloading the data
(Fieldwork events, Sites, and their Components) from
the Digital archive of the Archaeological Map of the
Czech Republic (https://digiarchiv.aiscr.cz/).
license: GPL-3.0
version: '1.0.1'
date-released: '2026-02-11'
version: '2.0.0'
date-released: '2026-06-05'
+6
View File
@@ -0,0 +1,6 @@
# CLAUDE.md
Pokyny pro tento repozitář jsou v [`AGENTS.md`](./AGENTS.md).
Tento soubor je záměrně jen odkaz, aby existoval jeden zdroj pravdy a obsah se
nerozjel. Cokoli platí pro AI agenty v tomto projektu, najdeš v `AGENTS.md`.
+5 -3
View File
@@ -2,7 +2,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
**Platform:** QGIS 3.4.0–4.99.0
**Platform:** QGIS 3.44.0–4.99.0
**Module Type:** Data Acquisition & Visualization
@@ -24,9 +24,9 @@
## 2. Installation Guide
**Install the plugin from QGIS plugin repository.**
**Install the latest version of the plugin from the QGIS plugin repository.**
**OR**
**OR** (*in case you need older version*)
1. *Obtain the [plugin distribution package](https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases) (ZIP archive containing the `amcr_viewer` directory).*
2. *Launch QGIS.*
@@ -131,6 +131,8 @@ Layers are only created if the query returns features of the corresponding geome
The plugin is developed in **Python 3** using the **PyQt6** framework for the GUI and the **Requests** library for HTTP communication.
> **Note:** The `requests` library is bundled with the QGIS installers for Windows and macOS. On Linux (distribution packages), it may need to be installed separately (e.g. `python3-requests`).
### 4.1 File Structure
* `amcr_viewer.py`: Entry point; handles GUI integration, toolbar/menu setup, and login flow.
+12
View File
@@ -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
-1
View File
@@ -31,6 +31,5 @@ def classFactory(iface): # pylint: disable=invalid-name
:param iface: A QGIS interface instance.
:type iface: QgsInterface
"""
#
from .amcr_viewer import AmcrViewer
return AmcrViewer(iface)
+91 -31
View File
@@ -1,32 +1,39 @@
# -*- coding: utf-8 -*-
import os
# -*- coding: utf-8 -*-
import csv
import requests
import xml.etree.ElementTree as ET # nosec
import os
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
PLUGIN_DIR = os.path.dirname(__file__)
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')
slovnicek = {
'obdobi': 'heslo:obdobi',
'typ_akce': 'heslo:akce_typ',
'areal': 'heslo:areal',
'kraj': 'ruian_kraj',
'organizace': 'organizace',
'okres': 'ruian_okres',
'katastr': 'ruian_katastr',
'vedouci': 'osoba',
'pian_presnost': 'heslo:pian_presnost',
'typ_lokality': 'heslo:lokalita_typ',
'druh_lokality': 'heslo:lokalita_druh',
'jistota': 'heslo:jistota_urceni',
'lokalita_zachovalost': 'heslo:stav_dochovani',
'pristupnost': 'heslo:pristupnost'
'obdobi': (BASE_URL_AMCR, 'heslo:obdobi'),
'typ_akce': (BASE_URL_AMCR, 'heslo:akce_typ'),
'areal': (BASE_URL_AMCR, 'heslo:areal'),
'kraj': (BASE_URL_AMCR, 'ruian_kraj'),
'organizace': (BASE_URL_AMCR, 'organizace'),
'okres': (BASE_URL_AMCR, 'ruian_okres'),
'katastr': (BASE_URL_AMCR, 'ruian_katastr'),
'pian_presnost': (BASE_URL_AMCR, 'heslo:pian_presnost'),
'typ_lokality': (BASE_URL_AMCR, 'heslo:lokalita_typ'),
'druh_lokality': (BASE_URL_AMCR, 'heslo:lokalita_druh'),
'jistota': (BASE_URL_AMCR, 'heslo:jistota_urceni'),
'lokalita_zachovalost': (BASE_URL_AMCR, 'heslo:stav_dochovani'),
'pristupnost': (BASE_URL_AMCR, '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 = {
@@ -58,7 +65,7 @@ def parse_codelist_file(filename, target_dict=None):
try:
# 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=';')
# Skip the CSV header row
@@ -84,7 +91,7 @@ def parse_codelist_file(filename, target_dict=None):
except Exception as e:
QgsMessageLog.logMessage(
f"AMČR Codelist Read Error for {filename}: {e}",
"AMČR", Qgis.Critical)
"AMČR", Qgis.MessageLevel.Critical)
return target_dict
@@ -92,18 +99,24 @@ def parse_codelist_file(filename, target_dict=None):
def load_all_data():
"""Loads the codelist during plugin startup."""
ensure_codelists_dir()
categorized_data = {k: {} for k in slovnicek.keys()}
categorized_data = {k: {} for k in slovnicek}
parse_codelist_file('heslar.csv', 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 = []
params = {
params_amcr = {
"verb": "ListRecords",
"metadataPrefix": "oai_dc",
"set": api_set
}
params_da = {
"entity": "samostatny_nalez" if internal_name == "nalezce" else "akce",
"rows": 0,
"noFacets": "false",
"onlyFacets": "true"
}
while True:
# Check for cancellation at each iteration
@@ -111,7 +124,10 @@ def fetch_set(internal_name, api_set, task=None):
return None
try:
response = requests.get(BASE_URL, params=params, timeout=30)
if "digiarchiv" not in base_url:
response = requests.get(
base_url, params=params_amcr, timeout=30
)
response.raise_for_status()
root = ET.fromstring(response.content) # nosec
@@ -158,6 +174,11 @@ def fetch_set(internal_name, api_set, task=None):
),
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,
@@ -168,7 +189,7 @@ def fetch_set(internal_name, api_set, task=None):
# Pagination
token = root.find('.//oai:resumptionToken', NS)
if token is not None and token.text:
params = {
params_amcr = {
"verb": "ListRecords",
"resumptionToken": token.text
}
@@ -176,10 +197,29 @@ def fetch_set(internal_name, api_set, task=None):
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({
'Název': nazev,
'Kód': nazev,
'Kategorie': internal_name
})
break
except Exception as e:
QgsMessageLog.logMessage(
f"Chyba u setu {api_set}: {e}",
"AMČR", Qgis.Warning)
"AMČR", Qgis.MessageLevel.Warning)
break
return dataset
@@ -190,18 +230,23 @@ def download_heslare(task=None):
ensure_codelists_dir()
all_data = []
total_sets = len(slovnicek)
# index, (interni, api_nazev)
for index, (key, value) in enumerate(slovnicek.items()):
base_url = value[0]
interni = key
api_nazev = value[1]
for index, (interni, api_nazev) in enumerate(slovnicek.items()):
# Check if the user cancelled the task via the QGIS taskbar
if task and task.isCanceled():
return False
QgsMessageLog.logMessage(
f"Zpracovávám kategorii: {interni}...",
"AMČR", Qgis.Info)
"AMČR", Qgis.MessageLevel.Info)
# 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:
return False # Cancelled mid-download
@@ -255,6 +300,16 @@ def refresh_globals():
LOKALITA_ZACHOVALOST.update(data.get('lokalita_zachovalost', {}))
PRISTUPNOST.clear()
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', {}))
# Initialize empty dicts that will be populated immediately below
@@ -272,5 +327,10 @@ DRUH_LOKALITY = {}
JISTOTA = {}
LOKALITA_ZACHOVALOST = {}
PRISTUPNOST = {}
NALEZ_KATEGORIE = {}
DRUH_NALEZU = {}
SPECIFIKACE = {}
NALEZOVE_OKOLNOSTI = {}
NALEZCE = {}
refresh_globals()
+388 -51
View File
@@ -1,22 +1,78 @@
# -*- coding: utf-8 -*-
from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
QLineEdit, QDialogButtonBox,
QCheckBox, QGroupBox, QPushButton,
QListWidget, QListWidgetItem, QHBoxLayout,
QMessageBox, QLabel, QFormLayout)
from qgis.PyQt.QtCore import Qt, QSettings
from qgis.core import (QgsTask, QgsApplication,
QgsMessageLog, Qgis, QgsAuthMethodConfig)
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
TYP_LOKALITY, DRUH_LOKALITY, JISTOTA,
LOKALITA_ZACHOVALOST, PRISTUPNOST,
download_heslare, refresh_globals)
# -*- coding: utf-8 -*-
from qgis.core import (
Qgis,
QgsApplication,
QgsAuthMethodConfig,
QgsMessageLog,
QgsTask,
)
from qgis.gui import QgsDateEdit
from qgis.PyQt.QtCore import QSettings, Qt
from qgis.PyQt.QtWidgets import (
QCheckBox,
QDialog,
QDialogButtonBox,
QFormLayout,
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidget,
QListWidgetItem,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
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):
def __init__(self, description):
super().__init__(description, QgsTask.CanCancel)
super().__init__(description, QgsTask.Flag.CanCancel)
self.success = False
self.exception = None
@@ -37,16 +93,19 @@ class UpdateCodelistsTask(QgsTask):
refresh_globals()
QgsMessageLog.logMessage(
"Hesláře AMČR byly úspěšně aktualizovány.",
"AMČR", Qgis.Info)
"AMČR", Qgis.MessageLevel.Info
)
else:
if self.isCanceled():
QgsMessageLog.logMessage(
"Aktualizace heslářů byla zrušena.",
"AMČR", Qgis.Warning)
"AMČR", Qgis.MessageLevel.Warning
)
else:
QgsMessageLog.logMessage(
f"Chyba aktualizace: {self.exception}",
"AMČR", Qgis.Critical)
"AMČR", Qgis.MessageLevel.Critical
)
class FilterableSelectionDialog(QDialog):
@@ -144,12 +203,31 @@ class AmcrFilterDialog(QDialog):
# Cache dictionary to store selected codes for each category
self.selection_cache = {
'organizace': [], 'kraj': [], 'obdobi': [], 'areal': [],
'typ_akce': [], 'okres': [], 'katastr': [], 'vedouci': [],
'pian_presnost': [], 'pristupnost': [], 'typ_lokality': [],
'druh_lokality': [], 'jistota': [], 'lokalita_zachovalost': []
'organizace': [],
'kraj': [],
'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()
# Filter by current map canvas extent
@@ -162,6 +240,8 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce":
self.chk_posevidence = QCheckBox("Pouze pozitivní zjištění")
layout.addWidget(self.chk_posevidence)
self.chk_proj_akce = QCheckBox("Pouze projektové akce")
layout.addWidget(self.chk_proj_akce)
layout.addSpacing(10)
@@ -180,13 +260,6 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_katastr)
self.picker_presnost = self.setup_picker(
"PIAN – přesnost",
'pian_presnost',
PIAN_PRESNOST
)
layout.addWidget(self.picker_presnost)
self.picker_pristupnost = self.setup_picker(
"Přístupnost",
'pristupnost',
@@ -196,7 +269,15 @@ class AmcrFilterDialog(QDialog):
# Filters valid for Akce
if self.typ_dat == "akce":
if self.typ_dat in ["lokalita", "akce"]:
self.picker_presnost = self.setup_picker(
"PIAN – přesnost",
'pian_presnost',
PIAN_PRESNOST
)
layout.addWidget(self.picker_presnost)
if self.typ_dat in ["samostatny_nalez", "akce"]:
self.picker_org = self.setup_picker(
"Organizace",
'organizace',
@@ -204,6 +285,7 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_org)
if self.typ_dat == "akce":
self.picker_vedouci = self.setup_picker(
"Vedoucí výzkumu",
'vedouci',
@@ -220,6 +302,12 @@ class AmcrFilterDialog(QDialog):
)
layout.addWidget(self.picker_typ)
self.box_datum = self.setup_date_range("Datum", [
("akce_datum_zahajeni", "Zahájení", "Datum zahájení"),
("akce_datum_ukonceni", "Ukončení", "Datum ukončení"),
])
layout.addWidget(self.box_datum)
# Filters valid for Lokality
if self.typ_dat == "lokalita":
@@ -256,6 +344,50 @@ class AmcrFilterDialog(QDialog):
self.picker_obdobi = self.setup_picker("Období", 'obdobi', OBDOBI)
layout.addWidget(self.picker_obdobi)
if self.typ_dat == "samostatny_nalez":
self.picker_nalez_kategorie = self.setup_picker(
"Kategorie nálezu",
'nalez_kategorie',
NALEZ_KATEGORIE
)
layout.addWidget(self.picker_nalez_kategorie)
self.picker_druh_nalezu = self.setup_picker(
"Druh nálezu",
'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)
@@ -277,6 +409,7 @@ class AmcrFilterDialog(QDialog):
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
)
@@ -284,6 +417,21 @@ class AmcrFilterDialog(QDialog):
# Pushes everything above to the top
layout.addStretch(1)
# The filter stack is taller than the window on every entity
# (over 1000 px for 'akce'), so it scrolls. The buttons stay
# outside the scroll area, otherwise the user would have to
# scroll to the bottom just to confirm the dialog.
content = QWidget()
content.setLayout(layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setWidget(content)
outer = QVBoxLayout()
outer.addWidget(scroll)
# Main dialog OK/Cancel/Update buttons
buttons = QDialogButtonBox()
@@ -304,9 +452,9 @@ class AmcrFilterDialog(QDialog):
buttons.accepted.connect(self.accept)
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):
"""
@@ -354,7 +502,7 @@ class AmcrFilterDialog(QDialog):
self.selection_cache[cache_key] = [
'HES-000861',
'HES-000862',
'HES-000863'
'HES-000863',
]
btn.clicked.connect(open_dialog)
@@ -369,32 +517,138 @@ class AmcrFilterDialog(QDialog):
row_widget.setLayout(row_layout)
return row_widget
def action_update_heslare(self):
# Create the task instance
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
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.
# Re-enable the button regardless of the outcome
task.taskCompleted.connect(lambda: self.btn_update.setEnabled(True))
task.taskTerminated.connect(lambda: self.btn_update.setEnabled(True))
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.
task.taskCompleted.connect(lambda: QMessageBox.information(
A picker left empty means an open bound; the sentinel is
substituted in get_filters(), not here, so that an untouched
block adds no filter at all.
"""
row_widget = QGroupBox(title)
grid = QGridLayout()
grid.setContentsMargins(5, 5, 5, 5)
grid.setVerticalSpacing(3)
grid.setHorizontalSpacing(6)
for row, (api_field, row_label, name) in enumerate(rows):
if row_label:
grid.addWidget(QLabel(row_label), row, 0)
date_from = self._date_edit(
f"{name} – od (prázdné = bez dolní meze)"
)
date_to = self._date_edit(
f"{name} – do (prázdné = bez horní meze)"
)
separator = QLabel("–")
separator.setAlignment(Qt.AlignmentFlag.AlignCenter)
grid.addWidget(date_from, row, 1)
grid.addWidget(separator, row, 2)
grid.addWidget(date_to, row, 3)
self.date_ranges.append((api_field, name, date_from, date_to))
grid.setColumnStretch(1, 1)
grid.setColumnStretch(3, 1)
row_widget.setLayout(grid)
return row_widget
@staticmethod
def _date_edit(tooltip):
"""
A date picker that may stay empty.
clear() is essential here – setEmpty() looks empty but leaves
isNull() False with today's date, which would silently apply
a filter the user never set.
"""
widget = QgsDateEdit()
widget.setAllowNull(True)
widget.setNullRepresentation(DATE_NULL_TEXT)
widget.setDisplayFormat("d. M. yyyy")
widget.setCalendarPopup(True)
widget.clear()
widget.setToolTip(tooltip)
return widget
def accept(self):
"""
Blocks the dialog on a reversed date range. The API answers such
a query with zero records and no error, which is indistinguishable
from a genuinely empty result.
"""
reversed_ranges = [
name for _, name, date_from, date_to in self.date_ranges
if not date_from.isNull() and not date_to.isNull()
and date_from.date() > date_to.date()
]
if reversed_ranges:
QMessageBox.warning(
self,
"Neplatné rozmezí",
"U těchto filtrů je počáteční datum novější než koncové:\n"
+ "\n".join(f"• {name}" for name in reversed_ranges)
+ "\n\nDotaz by nevrátil žádný záznam. Opravte rozmezí, "
"nebo jedno z polí vyprázdněte."
)
return
super().accept()
def action_update_heslare(self):
# Create the task instance and keep a reference so the Python
# wrapper survives until the task finishes
task = UpdateCodelistsTask("Aktualizace heslářů AMČR")
_ACTIVE_TASKS.append(task)
# Prevent parallel downloads overwriting heslar.csv
self.btn_update.setEnabled(False)
# 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
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():
_cleanup()
if task.exception:
# This will show exactly what went wrong (e.g. PermissionError)
msg = (
"Aktualizace selhala z důvodu chyby:\n"
f"{str(task.exception)}"
f"{task.exception!s}"
)
else:
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)
QgsApplication.taskManager().addTask(task)
@@ -403,7 +657,9 @@ class AmcrFilterDialog(QDialog):
return "true" if self.chk_bbox.isChecked() else "false"
def get_komponenty(self):
if self.typ_dat in ["akce", "lokalita"]:
return "true" if self.chk_komponenty.isChecked() else "false"
return "false"
def get_filters(self):
"""Compiles the user selections from the cache into
@@ -428,14 +684,17 @@ class AmcrFilterDialog(QDialog):
if self.typ_dat == "akce":
if self.chk_posevidence.isChecked():
filters['posevidence'] = 'true'
if self.selection_cache['organizace']:
filters['f_organizace'] = self.selection_cache['organizace']
if self.chk_proj_akce.isChecked():
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['organizace']:
filters['f_organizace'] = self.selection_cache['organizace']
if self.selection_cache['typ_lokality']:
filters['f_typ_lokality'] = self.selection_cache['typ_lokality']
if self.selection_cache['druh_lokality']:
@@ -443,7 +702,38 @@ class AmcrFilterDialog(QDialog):
if self.selection_cache['jistota']:
filters['f_jistota'] = self.selection_cache['jistota']
if self.selection_cache['lokalita_zachovalost']:
filters['f_lokalita_zachovalost'] = self.selection_cache['lokalita_zachovalost']
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
@@ -465,7 +755,8 @@ class LoginDialog(QDialog):
- storeAuthenticationConfig() and loadAuthenticationConfig() both have
SIP_INOUT on their config parameter, so Python bindings return a tuple
(bool, QgsAuthMethodConfig) rather than just bool. Always unpack both.
- loadAuthenticationConfig() with full=False loads only metadata (name, method,
- loadAuthenticationConfig() with full=False loads only metadata
(name, method,
id) but NOT the config() values like username/password. Use full=True to
access those.
"""
@@ -602,6 +893,39 @@ class LoginDialog(QDialog):
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
# ------------------------------------------------------------------
@@ -628,6 +952,11 @@ class LoginDialog(QDialog):
if ok:
if not self._ensure_master_password():
return
# Verify the new username against the stored password
if not self._verify_credentials(
username, cfg.config("password", "")
):
return
cfg.setConfig("username", username)
auth_mgr.updateAuthenticationConfig(cfg)
self.accept()
@@ -637,6 +966,11 @@ class LoginDialog(QDialog):
QMessageBox.warning(self, "Chybí údaje", "Vyplňte prosím heslo.")
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():
return
@@ -653,7 +987,7 @@ class LoginDialog(QDialog):
# We skip hasConfigId() as it may return False
# despite the config existing
# (in-memory cache may not be populated yet in QGIS 4).
ok_load, existing_cfg = (
ok_load, _ = (
self._load_config(existing_id, full=False)
if existing_id
else (False, None)
@@ -682,7 +1016,9 @@ class LoginDialog(QDialog):
settings = QSettings()
existing_id = settings.value(self.SETTINGS_KEY, "")
if existing_id:
QgsApplication.authManager().removeAuthenticationConfig(existing_id)
QgsApplication.authManager().removeAuthenticationConfig(
existing_id
)
settings.remove(self.SETTINGS_KEY)
QMessageBox.information(
self,
@@ -717,4 +1053,5 @@ class LoginDialog(QDialog):
if not ok:
return "", ""
return cfg.config("username", ""), cfg.config("password", "") # nosec B106
return (cfg.config("username", ""),
cfg.config("password", "")) # nosec B106
+556 -85
View File
File diff suppressed because it is too large. Load diff
+30 -17
View File
@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QUrl
from qgis.PyQt.QtGui import QIcon, QDesktopServices
from qgis.PyQt.QtWidgets import QMenu, QAction, QToolButton, QDialog
from qgis.core import Qgis
from .amcr_tools import load_amcr_data, login_to_api
from .amcr_dialog import AmcrFilterDialog, LoginDialog
from .resources import *
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:
"""
@@ -24,12 +24,13 @@ class AmcrViewer:
self.iface = iface
self.plugin_dir = os.path.dirname(__file__)
# Determine the user's locale to load appropriate translation files
locale = QSettings().value('locale/userLocale')[0:2]
# Determine the user's locale to load appropriate translation files.
# 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(
self.plugin_dir,
'i18n',
'AmcrViewer_{}.qm'.format(locale)
f'AmcrViewer_{locale}.qm'
)
# Install the translator if a translation file
@@ -41,7 +42,7 @@ class AmcrViewer:
# Initialize internal state
self.actions = []
self.menu = self.tr(u'&AMČR Viewer')
self.menu = self.tr('&AMČR Viewer')
self.first_start = None
def tr(self, message):
@@ -90,6 +91,7 @@ class AmcrViewer:
"""
# Define paths for action-specific icons
icon_akce_path = os.path.join(self.plugin_dir, 'akce.png')
icon_pas_path = os.path.join(self.plugin_dir, 'sn.png')
icon_lokality_path = os.path.join(self.plugin_dir, 'lokality.png')
icon_amcr_help_path = os.path.join(self.plugin_dir, 'amcr-help.png')
@@ -101,7 +103,7 @@ class AmcrViewer:
# custom dropdown menu
self.action_download_akce = self.add_action(
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'),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -109,9 +111,20 @@ class AmcrViewer:
)
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(
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'),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -120,8 +133,8 @@ class AmcrViewer:
self.plugin_menu.addAction(self.action_download_lokality)
self.action_login_dialog = self.add_action(
icon_path=icon_akce_path,
text=self.tr(u'Přihlásit se | AMČR Viewer'),
icon_path=icon_pas_path,
text=self.tr('Přihlásit se | AMČR Viewer'),
callback=lambda checked=False: self.login(),
parent=self.iface.mainWindow(),
add_to_menu=False,
@@ -131,7 +144,7 @@ class AmcrViewer:
self.action_amcr_help = self.add_action(
icon_path=icon_amcr_help_path,
text=self.tr(u'Nápověda AMČR Help | AMČR Viewer'),
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,
File diff suppressed because it is too large. Load diff
+20 -6
View File
@@ -5,10 +5,10 @@
[general]
name=AMČR Viewer
qgisMinimumVersion=3.4.0
qgisMinimumVersion=3.44.0
qgisMaximumVersion=4.99.0
description=Viewing and downloading the AMČR data.
version=2.0.0
version=2.1.2
author=David Spáčil
email=spacil@arub.cz
@@ -23,9 +23,23 @@ repository=https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer
hasProcessingProvider=no
# Uncomment the following line and add your changelog:
changelog=
Aktualizace na verzi 2.0.0 výrazně mění chování pluginu. Před updatem je doporučeno přečíst si seznam změn níže.
Version 2.0.0 changes the plugin behavior dramatically. It is advised to check the changelog below.
Plný seznam změn v češtině je dostupný zde: https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer/releases/tag/v2.0.0
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
@@ -45,7 +59,7 @@ tags=python,AMCR,AIS CR,archaeology,PIAN,AMČR,archeologie
homepage=https://amcr-help.aiscr.cz/digiarchiv/qgis-viewer.html
category=Vector
icon=download.png
icon=akce.png
# experimental flag
experimental=False
Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

+44
View File
@@ -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
View File
@@ -9,7 +9,7 @@
id="svg1"
xml:space="preserve"
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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@@ -24,12 +24,12 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="22.627417"
inkscape:cx="6.8059028"
inkscape:cy="14.606174"
inkscape:cx="0.57452426"
inkscape:cy="15.556349"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-height="1131"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
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 "
inkscape:export-filename="path32.png"
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

+85
View File
@@ -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ů")
+130
View File
@@ -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")