diff --git a/datasette/facets.py b/datasette/facets.py index 8c09e1dc..394eafad 100644 --- a/datasette/facets.py +++ b/datasette/facets.py @@ -264,10 +264,15 @@ class ColumnFacet(Facet): column_qs = column if column.startswith("_"): column_qs = f"{column}__exact" - selected = (column_qs, str(row["value"])) in qs_pairs + selected_args = { + key: str(row["value"]) + for key in (column_qs, f"{column}__exact") + if (key, str(row["value"])) in qs_pairs + } + selected = bool(selected_args) if selected: toggle_path = path_with_removed_args( - self.request, {column_qs: str(row["value"])} + self.request, selected_args ) else: toggle_path = path_with_added_args( diff --git a/docs/changelog.rst b/docs/changelog.rst index 814aeb7f..c431007c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,6 +26,7 @@ Datasette plugins can now use **background tasks** to run code independent of th Bug fixes ~~~~~~~~~ +- Column facets now show the remove-filter link for filters using ``column__exact=value``, as well as ``column=value``. (:issue:`1695`) - The :ref:`alter-table API ` now rolls back schema changes when a :ref:`write_wrapper ` raises after the write. (:issue:`2924`, :pr:`2925`) - The :ref:`extra_template_vars() ` plugin hook can now return a function or awaitable that resolves to ``None`` when no extra variables are needed. (:issue:`2005`) - :ref:`request.headers ` now supports case-insensitive header lookups, so ``request.headers.get("Content-Type")`` works as well as ``request.headers.get("content-type")``. (:issue:`1861`) diff --git a/tests/test_facets.py b/tests/test_facets.py index b8eb6e61..eccfbb93 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -1,4 +1,5 @@ import json +from urllib.parse import parse_qsl, urlsplit import pytest @@ -151,6 +152,63 @@ async def test_column_facet_results(ds_client): ] == buckets +@pytest.mark.asyncio +@pytest.mark.parametrize( + "column,filters,remaining", + [ + ("COUNTY", "COUNTY=Lee", []), + ("COUNTY", "COUNTY__exact=Lee", []), + ("COUNTY", "COUNTY=Lee&COUNTY__exact=Lee", []), + ("COUNTY", "COUNTY__exact=Lee&COUNTY__exact=Lee", []), + ( + "COUNTY", + "COUNTY__exact=Lee&COUNTY__exact=Polk", + [("COUNTY__exact", "Polk")], + ), + ("_county", "_county__exact=Lee", []), + ("_county", "_county__exact=Lee&_county=Lee", [("_county", "Lee")]), + ], +) +async def test_column_facet_selected_exact_filters( + ds_client, column, filters, remaining +): + facet = ColumnFacet( + ds_client.ds, + Request.fake(f"/?_facet={column}&{filters}&other=keep&_sort={column}"), + database="fixtures", + sql=f"select 'Lee' as {column}", + ) + buckets, timed_out = await facet.facet_results() + assert not timed_out + result = buckets[0]["results"][0] + assert result["selected"] is True + assert parse_qsl(urlsplit(result["toggle_url"]).query) == [ + ("_facet", column), + *remaining, + ("other", "keep"), + ("_sort", column), + ] + + +@pytest.mark.asyncio +async def test_column_facet_underscore_argument_is_not_a_filter(ds_client): + facet = ColumnFacet( + ds_client.ds, + Request.fake("/?_facet=_county&_county=Lee"), + database="fixtures", + sql="select 'Lee' as _county", + ) + buckets, timed_out = await facet.facet_results() + assert not timed_out + result = buckets[0]["results"][0] + assert result["selected"] is False + assert parse_qsl(urlsplit(result["toggle_url"]).query) == [ + ("_facet", "_county"), + ("_county", "Lee"), + ("_county__exact", "Lee"), + ] + + @pytest.mark.asyncio async def test_column_facet_results_column_starts_with_underscore(ds_client): facet = ColumnFacet( diff --git a/tests/test_table_html.py b/tests/test_table_html.py index f78e532f..0bdc7282 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -484,6 +484,31 @@ async def test_facet_display(ds_client): ] +@pytest.mark.asyncio +@pytest.mark.parametrize( + "filters", ["state=CA", "state__exact=CA", "state=CA&state__exact=CA"] +) +async def test_facet_remove_exact_filter(ds_client, filters): + response = await ds_client.get( + f"/fixtures/facetable?_facet=state&{filters}&on_earth=1" + ) + assert response.status_code == 200 + soup = Soup(response.text, "html.parser") + remove_link = soup.select_one('.facet-info[data-column="state"] li a.cross') + assert remove_link is not None + url = urllib.parse.urlsplit(remove_link["href"]) + assert urllib.parse.parse_qsl(url.query) == [ + ("_facet", "state"), + ("on_earth", "1"), + ] + unfiltered = await ds_client.get(f"{url.path}.json?{url.query}") + assert unfiltered.status_code == 200 + rows = unfiltered.json()["rows"] + assert len(rows) == 14 + assert all(row["on_earth"] == 1 for row in rows) + assert {row["state"] for row in rows} == {"CA", "MI"} + + @pytest.mark.asyncio async def test_facets_persist_through_filter_form(ds_client): response = await ds_client.get(