diff --git a/datasette/utils.py b/datasette/utils.py index 118d9bd7..0e0c388b 100644 --- a/datasette/utils.py +++ b/datasette/utils.py @@ -165,6 +165,18 @@ def path_with_added_args(request, args, path=None): return path + query_string +def path_with_removed_args(request, args, path=None): + path = path or request.path + current = [] + for key, value in urllib.parse.parse_qsl(request.query_string): + if key not in args: + current.append((key, value)) + query_string = urllib.parse.urlencode(current) + if query_string: + query_string = '?{}'.format(query_string) + return path + query_string + + def path_with_ext(request, ext): path = request.path path += ext diff --git a/datasette/views/table.py b/datasette/views/table.py index 5369825c..99075abe 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -13,6 +13,7 @@ from datasette.utils import ( is_url, path_from_row_pks, path_with_added_args, + path_with_removed_args, to_css_class, urlsafe_components ) @@ -505,17 +506,25 @@ class TableView(RowTableShared): facet_rows = await self.execute( name, facet_sql, params, truncate=False, custom_time_limit=200 ) - facet_results[column] = [ - { + facet_results[column] = [] + for row in facet_rows: + selected = other_args.get(column) == row["value"] + if selected: + toggle_path = path_with_removed_args( + request, {column: row["value"]} + ) + else: + toggle_path = path_with_added_args( + request, {column: row["value"]} + ) + facet_results[column].append({ "value": row["value"], "count": row["count"], "toggle_url": urllib.parse.urljoin( - request.url, - path_with_added_args(request, {column: row["value"]}), + request.url, toggle_path ), - } - for row in facet_rows - ] + "selected": selected, + }) except sqlite3.OperationalError: # Hit time limit pass diff --git a/docs/facets.rst b/docs/facets.rst index 40499e55..02ac8d55 100644 --- a/docs/facets.rst +++ b/docs/facets.rst @@ -23,11 +23,13 @@ This works for both the HTML interface and the ``.json`` view. When enabled, fac { "value": "CA", "count": 10, + "selected": false, "toggle_url": "http://...&state=CA" }, { "value": "MI", "count": 4, + "selected": false, "toggle_url": "http://...&state=MI" } ], @@ -35,16 +37,19 @@ This works for both the HTML interface and the ``.json`` view. When enabled, fac { "value": "San Francisco", "count": 6, + "selected": false, "toggle_url": "http://...=San+Francisco" }, { "value": "Detroit", "count": 4, + "selected": false, "toggle_url": "http://...&city=Detroit" }, { "value": "Los Angeles", "count": 4, + "selected": false, "toggle_url": "http://...=Los+Angeles" } ] diff --git a/tests/test_api.py b/tests/test_api.py index 3f91b075..da8c177c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -897,11 +897,13 @@ def test_page_size_matching_max_returned_rows(app_client_returend_rows_matches_p { "value": "CA", "count": 10, + "selected": False, "toggle_url": "_facet=state&_facet=city&state=CA", }, { "value": "MI", "count": 4, + "selected": False, "toggle_url": "_facet=state&_facet=city&state=MI", }, ], @@ -909,16 +911,19 @@ def test_page_size_matching_max_returned_rows(app_client_returend_rows_matches_p { "value": "San Francisco", "count": 6, + "selected": False, "toggle_url": "_facet=state&_facet=city&city=San+Francisco", }, { "value": "Detroit", "count": 4, + "selected": False, "toggle_url": "_facet=state&_facet=city&city=Detroit", }, { "value": "Los Angeles", "count": 4, + "selected": False, "toggle_url": "_facet=state&_facet=city&city=Los+Angeles", }, ], @@ -930,13 +935,15 @@ def test_page_size_matching_max_returned_rows(app_client_returend_rows_matches_p { "value": "MI", "count": 4, - "toggle_url": "_facet=state&_facet=city&state=MI", + "selected": True, + "toggle_url": "_facet=state&_facet=city", }, ], "city": [ { "value": "Detroit", "count": 4, + "selected": False, "toggle_url": "_facet=state&_facet=city&state=MI&city=Detroit", }, ], diff --git a/tests/test_utils.py b/tests/test_utils.py index 3f51f87d..0d6bb2d1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -45,6 +45,19 @@ def test_path_with_added_args(path, added_args, expected): assert expected == actual +@pytest.mark.parametrize('path,args,expected', [ + ('/foo?bar=1', {'bar'}, '/foo'), + ('/foo?bar=1&baz=2', {'bar'}, '/foo?baz=2'), +]) +def test_path_with_removed_args(path, args, expected): + request = Request( + path.encode('utf8'), + {}, '1.1', 'GET', None + ) + actual = utils.path_with_removed_args(request, args) + assert expected == actual + + @pytest.mark.parametrize('row,pks,expected_path', [ ({'A': 'foo', 'B': 'bar'}, ['A', 'B'], 'foo,bar'), ({'A': 'f,o', 'B': 'bar'}, ['A', 'B'], 'f%2Co,bar'),