mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
Merge pull request #72 from zvx-echo6/v0.9.11-hide-tombstones
Hide tombstones from default events view + show-removed toggle (v0.9.11)
This commit is contained in:
commit
c772d117d0
4 changed files with 116 additions and 2 deletions
|
|
@ -2896,7 +2896,8 @@ def _build_pagination(total: int | None, offset: int, limit: int) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def _parse_events_params(params, default_time: str | None = None,
|
def _parse_events_params(params, default_time: str | None = None,
|
||||||
default_offset: int | None = None) -> tuple[dict | None, str | None]:
|
default_offset: int | None = None,
|
||||||
|
default_show_removed: bool = True) -> tuple[dict | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Parse and validate events query parameters.
|
Parse and validate events query parameters.
|
||||||
|
|
||||||
|
|
@ -3040,8 +3041,17 @@ def _parse_events_params(params, default_time: str | None = None,
|
||||||
except Exception:
|
except Exception:
|
||||||
return None, "Invalid cursor"
|
return None, "Invalid cursor"
|
||||||
|
|
||||||
|
# Tombstone visibility: the GUI default-hides `*.removed` events; the JSON
|
||||||
|
# API default-includes them. An explicit ?show_removed= overrides the default.
|
||||||
|
sr = params.get("show_removed")
|
||||||
|
if sr is not None and sr != "":
|
||||||
|
show_removed = sr.lower() in ("1", "true", "on")
|
||||||
|
else:
|
||||||
|
show_removed = default_show_removed
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
|
"show_removed": show_removed,
|
||||||
"offset": offset,
|
"offset": offset,
|
||||||
"q": q,
|
"q": q,
|
||||||
"adapters": adapters,
|
"adapters": adapters,
|
||||||
|
|
@ -3172,6 +3182,11 @@ async def _fetch_events(parsed_params: dict) -> EventsQueryResult:
|
||||||
query_params.append(cursor_id)
|
query_params.append(cursor_id)
|
||||||
param_idx += 2
|
param_idx += 2
|
||||||
|
|
||||||
|
# GUI hides tombstones by default (events.json includes them). Static
|
||||||
|
# literal pattern, no bound param; NULL-category rows are kept.
|
||||||
|
if not parsed_params.get("show_removed", True):
|
||||||
|
conditions.append("(category IS NULL OR category NOT LIKE '%.removed')")
|
||||||
|
|
||||||
where_clause = ""
|
where_clause = ""
|
||||||
if conditions:
|
if conditions:
|
||||||
where_clause = "WHERE " + " AND ".join(conditions)
|
where_clause = "WHERE " + " AND ".join(conditions)
|
||||||
|
|
@ -3361,7 +3376,8 @@ async def _events_query(request: Request, data_class: str):
|
||||||
which _fetch_events applies as an `adapter = ANY(...)` condition.
|
which _fetch_events applies as an `adapter = ANY(...)` condition.
|
||||||
"""
|
"""
|
||||||
params = request.query_params
|
params = request.query_params
|
||||||
parsed, error = _parse_events_params(params, default_time=DEFAULT_TIME, default_offset=0)
|
parsed, error = _parse_events_params(params, default_time=DEFAULT_TIME, default_offset=0,
|
||||||
|
default_show_removed=False)
|
||||||
class_adapters = _class_adapter_names(data_class)
|
class_adapters = _class_adapter_names(data_class)
|
||||||
if parsed is not None:
|
if parsed is not None:
|
||||||
parsed["class_adapters"] = class_adapters
|
parsed["class_adapters"] = class_adapters
|
||||||
|
|
@ -3392,6 +3408,7 @@ def _events_filter_state(parsed: dict | None, params) -> dict:
|
||||||
"region_west": params.get("region_west", ""),
|
"region_west": params.get("region_west", ""),
|
||||||
"map_filter": pstate.get("map_filter", False),
|
"map_filter": pstate.get("map_filter", False),
|
||||||
"limit": str(pstate.get("limit", 50)),
|
"limit": str(pstate.get("limit", 50)),
|
||||||
|
"show_removed": pstate.get("show_removed", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,16 @@
|
||||||
map-controls checkbox below syncs + enables it. #}
|
map-controls checkbox below syncs + enables it. #}
|
||||||
<input type="hidden" name="map_filter" id="filter-map_filter" value="1"
|
<input type="hidden" name="map_filter" id="filter-map_filter" value="1"
|
||||||
{{ '' if filter_state.map_filter else 'disabled' }}>
|
{{ '' if filter_state.map_filter else 'disabled' }}>
|
||||||
|
{# Tombstone visibility: enabled only when "Show removed" is on, so it's
|
||||||
|
omitted from the URL by default (GUI default-hides *.removed events). #}
|
||||||
|
<input type="hidden" name="show_removed" id="filter-show_removed" value="1"
|
||||||
|
{{ '' if filter_state.show_removed else 'disabled' }}>
|
||||||
|
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
|
<label class="show-removed-toggle">
|
||||||
|
<input type="checkbox" id="show-removed-toggle" {{ 'checked' if filter_state.show_removed else '' }}>
|
||||||
|
Show removed
|
||||||
|
</label>
|
||||||
<a href="{{ base_path }}" role="button" class="btn-outline" id="filter-clear-all">Clear all</a>
|
<a href="{{ base_path }}" role="button" class="btn-outline" id="filter-clear-all">Clear all</a>
|
||||||
<button type="submit" class="filter-apply">Apply</button>
|
<button type="submit" class="filter-apply">Apply</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -714,6 +722,18 @@
|
||||||
submitForm();
|
submitForm();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// "Show removed" toggle: enable the hidden input only when checked (so it's
|
||||||
|
// omitted from the URL by default), then re-query. The rows fragment's
|
||||||
|
// HX-Push-Url preserves the choice in a shared link.
|
||||||
|
var showRemovedToggle = document.getElementById("show-removed-toggle");
|
||||||
|
var showRemovedHidden = document.getElementById("filter-show_removed");
|
||||||
|
if (showRemovedToggle && showRemovedHidden) {
|
||||||
|
showRemovedToggle.addEventListener("change", function () {
|
||||||
|
showRemovedHidden.disabled = !showRemovedToggle.checked;
|
||||||
|
submitForm();
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1227,3 +1227,38 @@ class TestEventsJsonSubject:
|
||||||
def test_missing_source_fields_yields_none(self):
|
def test_missing_source_fields_yields_none(self):
|
||||||
"""An event lacking its adapter's source fields derives no subject."""
|
"""An event lacking its adapter's source fields derives no subject."""
|
||||||
assert _derive_subject(_subject_event("usgs_quake", {})) is None
|
assert _derive_subject(_subject_event("usgs_quake", {})) is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestShowRemovedToggle:
|
||||||
|
"""v0.9.11 — tombstone visibility wiring. filter_state.show_removed drives
|
||||||
|
the 'Show removed' checkbox in events_list.html; defaults to hidden."""
|
||||||
|
|
||||||
|
def _pool(self):
|
||||||
|
conn = AsyncMock()
|
||||||
|
conn.fetch.return_value = []
|
||||||
|
conn.fetchrow.return_value = {
|
||||||
|
"map_tile_url": "https://t/{z}/{x}/{y}.png", "map_attribution": "OSM"}
|
||||||
|
pool = MagicMock()
|
||||||
|
pool.acquire.return_value.__aenter__ = AsyncMock(return_value=conn)
|
||||||
|
pool.acquire.return_value.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
return pool
|
||||||
|
|
||||||
|
async def _filter_state(self, query_params):
|
||||||
|
req = MagicMock()
|
||||||
|
req.state.operator = MagicMock(id=1, username="admin")
|
||||||
|
req.state.csrf_token = "t"
|
||||||
|
req.query_params = query_params
|
||||||
|
templates = MagicMock()
|
||||||
|
templates.TemplateResponse.return_value = MagicMock(status_code=200)
|
||||||
|
with patch("central.gui.routes._get_templates", return_value=templates):
|
||||||
|
with patch("central.gui.routes.get_pool", return_value=self._pool()):
|
||||||
|
await events_list(req)
|
||||||
|
return templates.TemplateResponse.call_args.kwargs["context"]["filter_state"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_default_hides_removed(self):
|
||||||
|
assert (await self._filter_state({}))["show_removed"] is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_show_removed_true_reflected(self):
|
||||||
|
assert (await self._filter_state({"show_removed": "true"}))["show_removed"] is True
|
||||||
|
|
|
||||||
|
|
@ -251,3 +251,45 @@ async def test_select_includes_severity_column():
|
||||||
parsed, _ = routes._parse_events_params({"time": "all"})
|
parsed, _ = routes._parse_events_params({"time": "all"})
|
||||||
cap = await _capture(parsed)
|
cap = await _capture(parsed)
|
||||||
assert "severity" in cap["query"]
|
assert "severity" in cap["query"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- show_removed / tombstone visibility (v0.9.11) --------------------------
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_gui_default_excludes_removed():
|
||||||
|
"""GUI passes default_show_removed=False -> *.removed hidden by default."""
|
||||||
|
parsed, _ = routes._parse_events_params(
|
||||||
|
{"time": "all"}, default_time="last_24h", default_show_removed=False)
|
||||||
|
assert parsed["show_removed"] is False
|
||||||
|
cap = await _capture(parsed)
|
||||||
|
assert "category NOT LIKE '%.removed'" in cap["query"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_show_removed_true_includes_all():
|
||||||
|
"""?show_removed=true -> no tombstone exclusion in the query."""
|
||||||
|
parsed, _ = routes._parse_events_params(
|
||||||
|
{"time": "all", "show_removed": "true"}, default_show_removed=False)
|
||||||
|
assert parsed["show_removed"] is True
|
||||||
|
cap = await _capture(parsed)
|
||||||
|
assert ".removed" not in cap["query"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_json_api_includes_removed_by_default():
|
||||||
|
"""events.json calls the parser bare (default True) -> API output unchanged."""
|
||||||
|
parsed, _ = routes._parse_events_params({"time": "all"})
|
||||||
|
assert parsed["show_removed"] is True
|
||||||
|
cap = await _capture(parsed)
|
||||||
|
assert ".removed" not in cap["query"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_show_removed_composes_with_other_filters():
|
||||||
|
"""The tombstone exclusion coexists with adapter/time filters."""
|
||||||
|
parsed, _ = routes._parse_events_params(
|
||||||
|
{"adapter": "wfigs_perimeters", "time": "all"}, default_show_removed=False)
|
||||||
|
cap = await _capture(parsed)
|
||||||
|
assert "adapter = ANY($" in cap["query"]
|
||||||
|
assert "category NOT LIKE '%.removed'" in cap["query"]
|
||||||
|
assert ["wfigs_perimeters"] in cap["params"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue