mirror of
https://github.com/ARUP-CAS/aiscr-qgis-amcr-viewer.git
synced 2026-10-10 04:47:36 +02:00
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á.
This commit is contained in:
1 parent
4ed99d73d9
commit
9ec71c7ed3
1 file changed
+150
-3
+150
-3
@@ -3,10 +3,12 @@ from qgis.PyQt.QtWidgets import (QDialog, QVBoxLayout,
|
|||||||
QLineEdit, QDialogButtonBox,
|
QLineEdit, QDialogButtonBox,
|
||||||
QCheckBox, QGroupBox, QPushButton,
|
QCheckBox, QGroupBox, QPushButton,
|
||||||
QListWidget, QListWidgetItem, QHBoxLayout,
|
QListWidget, QListWidgetItem, QHBoxLayout,
|
||||||
QMessageBox, QLabel, QFormLayout)
|
QMessageBox, QLabel, QFormLayout,
|
||||||
|
QGridLayout, QScrollArea, QFrame, QWidget)
|
||||||
from qgis.PyQt.QtCore import Qt, QSettings
|
from qgis.PyQt.QtCore import Qt, QSettings
|
||||||
from qgis.core import (QgsTask, QgsApplication,
|
from qgis.core import (QgsTask, QgsApplication,
|
||||||
QgsMessageLog, Qgis, QgsAuthMethodConfig)
|
QgsMessageLog, Qgis, QgsAuthMethodConfig)
|
||||||
|
from qgis.gui import QgsDateEdit
|
||||||
from qgis.utils import iface
|
from qgis.utils import iface
|
||||||
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
||||||
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
|
OKRESY, KATASTRY, VEDOUCI, PIAN_PRESNOST,
|
||||||
@@ -17,6 +19,17 @@ from .amcr_codelists import (OBDOBI, TYP_AKCE, KRAJE, AREAL, ORGANIZACE,
|
|||||||
download_heslare, refresh_globals)
|
download_heslare, refresh_globals)
|
||||||
|
|
||||||
|
|
||||||
|
# The date filter of the API requires both bounds; a one-sided range makes
|
||||||
|
# the server fail with an ArrayIndexOutOfBoundsException and '*' is not
|
||||||
|
# accepted either. An empty picker is therefore replaced by these sentinels,
|
||||||
|
# which are also the default limits of QgsDateEdit.
|
||||||
|
DATE_OPEN_FROM = "0001-01-01"
|
||||||
|
DATE_OPEN_TO = "9999-12-31"
|
||||||
|
|
||||||
|
# Shown by a date picker that is left empty
|
||||||
|
DATE_NULL_TEXT = "neomezeno"
|
||||||
|
|
||||||
|
|
||||||
# Keep Python references to running tasks. QgsTaskManager only holds the
|
# Keep Python references to running tasks. QgsTaskManager only holds the
|
||||||
# C++ object; without a Python-side reference the wrapper can be garbage
|
# C++ object; without a Python-side reference the wrapper can be garbage
|
||||||
# collected before the task finishes, which crashes QGIS.
|
# collected before the task finishes, which crashes QGIS.
|
||||||
@@ -177,6 +190,10 @@ class AmcrFilterDialog(QDialog):
|
|||||||
'nalezce': [],
|
'nalezce': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Date range pickers, filled by setup_date_range():
|
||||||
|
# (API field, label for messages, 'from' widget, 'to' widget)
|
||||||
|
self.date_ranges = []
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Filter by current map canvas extent
|
# Filter by current map canvas extent
|
||||||
@@ -251,6 +268,12 @@ class AmcrFilterDialog(QDialog):
|
|||||||
)
|
)
|
||||||
layout.addWidget(self.picker_typ)
|
layout.addWidget(self.picker_typ)
|
||||||
|
|
||||||
|
self.box_datum = self.setup_date_range("Datum", [
|
||||||
|
("akce_datum_zahajeni", "Zahájení", "Datum zahájení"),
|
||||||
|
("akce_datum_ukonceni", "Ukončení", "Datum ukončení"),
|
||||||
|
])
|
||||||
|
layout.addWidget(self.box_datum)
|
||||||
|
|
||||||
# Filters valid for Lokality
|
# Filters valid for Lokality
|
||||||
|
|
||||||
if self.typ_dat == "lokalita":
|
if self.typ_dat == "lokalita":
|
||||||
@@ -323,6 +346,13 @@ class AmcrFilterDialog(QDialog):
|
|||||||
)
|
)
|
||||||
layout.addWidget(self.picker_nalezce)
|
layout.addWidget(self.picker_nalezce)
|
||||||
|
|
||||||
|
# Lokalita has no date field in the index at all, so the block
|
||||||
|
# is built only for the two entities that do
|
||||||
|
self.box_datum = self.setup_date_range("Datum nálezu", [
|
||||||
|
("samostatny_nalez_datum_nalezu", "", "Datum nálezu"),
|
||||||
|
])
|
||||||
|
layout.addWidget(self.box_datum)
|
||||||
|
|
||||||
if self.typ_dat != "samostatny_nalez":
|
if self.typ_dat != "samostatny_nalez":
|
||||||
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
self.picker_areal = self.setup_picker("Areál", 'areal', AREAL)
|
||||||
layout.addWidget(self.picker_areal)
|
layout.addWidget(self.picker_areal)
|
||||||
@@ -353,6 +383,21 @@ class AmcrFilterDialog(QDialog):
|
|||||||
# Pushes everything above to the top
|
# Pushes everything above to the top
|
||||||
layout.addStretch(1)
|
layout.addStretch(1)
|
||||||
|
|
||||||
|
# The filter stack is taller than the window on every entity
|
||||||
|
# (over 1000 px for 'akce'), so it scrolls. The buttons stay
|
||||||
|
# outside the scroll area, otherwise the user would have to
|
||||||
|
# scroll to the bottom just to confirm the dialog.
|
||||||
|
content = QWidget()
|
||||||
|
content.setLayout(layout)
|
||||||
|
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
scroll.setWidget(content)
|
||||||
|
|
||||||
|
outer = QVBoxLayout()
|
||||||
|
outer.addWidget(scroll)
|
||||||
|
|
||||||
# Main dialog OK/Cancel/Update buttons
|
# Main dialog OK/Cancel/Update buttons
|
||||||
|
|
||||||
buttons = QDialogButtonBox()
|
buttons = QDialogButtonBox()
|
||||||
@@ -373,9 +418,9 @@ class AmcrFilterDialog(QDialog):
|
|||||||
|
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@@ -438,6 +483,94 @@ class AmcrFilterDialog(QDialog):
|
|||||||
row_widget.setLayout(row_layout)
|
row_widget.setLayout(row_layout)
|
||||||
return row_widget
|
return row_widget
|
||||||
|
|
||||||
|
def setup_date_range(self, title, rows):
|
||||||
|
"""
|
||||||
|
Creates a compact date range block: one row per API date field,
|
||||||
|
each with a 'from' and a 'to' picker.
|
||||||
|
|
||||||
|
rows is a list of (api_field, row_label, name_for_messages).
|
||||||
|
An empty row_label is used when the group box title already names
|
||||||
|
the field, which keeps the single-row variant from repeating itself.
|
||||||
|
|
||||||
|
A picker left empty means an open bound; the sentinel is
|
||||||
|
substituted in get_filters(), not here, so that an untouched
|
||||||
|
block adds no filter at all.
|
||||||
|
"""
|
||||||
|
row_widget = QGroupBox(title)
|
||||||
|
grid = QGridLayout()
|
||||||
|
grid.setContentsMargins(5, 5, 5, 5)
|
||||||
|
grid.setVerticalSpacing(3)
|
||||||
|
grid.setHorizontalSpacing(6)
|
||||||
|
|
||||||
|
for row, (api_field, row_label, name) in enumerate(rows):
|
||||||
|
if row_label:
|
||||||
|
grid.addWidget(QLabel(row_label), row, 0)
|
||||||
|
|
||||||
|
date_from = self._date_edit(
|
||||||
|
f"{name} – od (prázdné = bez dolní meze)"
|
||||||
|
)
|
||||||
|
date_to = self._date_edit(
|
||||||
|
f"{name} – do (prázdné = bez horní meze)"
|
||||||
|
)
|
||||||
|
|
||||||
|
separator = QLabel("–")
|
||||||
|
separator.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
grid.addWidget(date_from, row, 1)
|
||||||
|
grid.addWidget(separator, row, 2)
|
||||||
|
grid.addWidget(date_to, row, 3)
|
||||||
|
|
||||||
|
self.date_ranges.append((api_field, name, date_from, date_to))
|
||||||
|
|
||||||
|
grid.setColumnStretch(1, 1)
|
||||||
|
grid.setColumnStretch(3, 1)
|
||||||
|
|
||||||
|
row_widget.setLayout(grid)
|
||||||
|
return row_widget
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _date_edit(tooltip):
|
||||||
|
"""
|
||||||
|
A date picker that may stay empty.
|
||||||
|
|
||||||
|
clear() is essential here – setEmpty() looks empty but leaves
|
||||||
|
isNull() False with today's date, which would silently apply
|
||||||
|
a filter the user never set.
|
||||||
|
"""
|
||||||
|
widget = QgsDateEdit()
|
||||||
|
widget.setAllowNull(True)
|
||||||
|
widget.setNullRepresentation(DATE_NULL_TEXT)
|
||||||
|
widget.setDisplayFormat("d. M. yyyy")
|
||||||
|
widget.setCalendarPopup(True)
|
||||||
|
widget.clear()
|
||||||
|
widget.setToolTip(tooltip)
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""
|
||||||
|
Blocks the dialog on a reversed date range. The API answers such
|
||||||
|
a query with zero records and no error, which is indistinguishable
|
||||||
|
from a genuinely empty result.
|
||||||
|
"""
|
||||||
|
reversed_ranges = [
|
||||||
|
name for _, name, date_from, date_to in self.date_ranges
|
||||||
|
if not date_from.isNull() and not date_to.isNull()
|
||||||
|
and date_from.date() > date_to.date()
|
||||||
|
]
|
||||||
|
|
||||||
|
if reversed_ranges:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Neplatné rozmezí",
|
||||||
|
"U těchto filtrů je počáteční datum novější než koncové:\n"
|
||||||
|
+ "\n".join(f"• {name}" for name in reversed_ranges)
|
||||||
|
+ "\n\nDotaz by nevrátil žádný záznam. Opravte rozmezí, "
|
||||||
|
"nebo jedno z polí vyprázdněte."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
super().accept()
|
||||||
|
|
||||||
def action_update_heslare(self):
|
def action_update_heslare(self):
|
||||||
# Create the task instance and keep a reference so the Python
|
# Create the task instance and keep a reference so the Python
|
||||||
# wrapper survives until the task finishes
|
# wrapper survives until the task finishes
|
||||||
@@ -550,6 +683,20 @@ class AmcrFilterDialog(QDialog):
|
|||||||
if self.selection_cache['nalezce']:
|
if self.selection_cache['nalezce']:
|
||||||
filters['f_nalezce'] = 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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user