From 9c77e6e355ec718d76178a7607721d10a66b6aef Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 15 Apr 2019 16:44:17 -0700 Subject: [PATCH] Support multiple filters of the same type Closes #288 --- datasette/views/table.py | 9 +++-- tests/test_api.py | 10 +++++ tests/test_filters.py | 87 ++++++++++++++++++++++------------------ 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/datasette/views/table.py b/datasette/views/table.py index 2c356bda..bc5e775e 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -219,13 +219,14 @@ class TableView(RowTableShared): # it can still be queried using ?_col__exact=blah special_args = {} special_args_lists = {} - other_args = {} + other_args = [] for key, value in args.items(): if key.startswith("_") and "__" not in key: special_args[key] = value[0] special_args_lists[key] = value else: - other_args[key] = value[0] + for v in value: + other_args.append((key, v)) # Handle ?_filter_column and redirect, if present redirect_params = filters_should_redirect(special_args) @@ -253,7 +254,7 @@ class TableView(RowTableShared): table_metadata = self.ds.table_metadata(database, table) units = table_metadata.get("units", {}) - filters = Filters(sorted(other_args.items()), units, ureg) + filters = Filters(sorted(other_args), units, ureg) where_clauses, params = filters.build_where_clauses(table) extra_wheres_for_ui = [] @@ -521,7 +522,7 @@ class TableView(RowTableShared): database, table, column, values )) for row in facet_rows: - selected = str(other_args.get(column)) == str(row["value"]) + selected = (column, str(row["value"])) in other_args if selected: toggle_path = path_with_removed_args( request, {column: str(row["value"])} diff --git a/tests/test_api.py b/tests/test_api.py index d6f612c8..53bf1d6e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -903,6 +903,16 @@ def test_table_filter_queries(app_client, path, expected_rows): assert expected_rows == response.json['rows'] +def test_table_filter_queries_multiple_of_same_type(app_client): + response = app_client.get( + "/fixtures/simple_primary_key.json?content__not=world&content__not=hello" + ) + assert [ + ['3', ''], + ['4', 'RENDER_CELL_DEMO'] + ] == response.json['rows'] + + @pytest.mark.skipif( not detect_json1(), reason="Requires the SQLite json1 module" diff --git a/tests/test_filters.py b/tests/test_filters.py index 7b19c4e9..a905dd2e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -4,88 +4,97 @@ import pytest @pytest.mark.parametrize('args,expected_where,expected_params', [ ( - { - 'name_english__contains': 'foo', - }, + ( + ('name_english__contains', 'foo'), + ), ['"name_english" like :p0'], ['%foo%'] ), ( - { - 'foo': 'bar', - 'bar__contains': 'baz', - }, + ( + ('foo', 'bar'), + ('bar__contains', 'baz'), + ), ['"bar" like :p0', '"foo" = :p1'], ['%baz%', 'bar'] ), ( - { - 'foo__startswith': 'bar', - 'bar__endswith': 'baz', - }, + ( + ('foo__startswith', 'bar'), + ('bar__endswith', 'baz'), + ), ['"bar" like :p0', '"foo" like :p1'], ['%baz', 'bar%'] ), ( - { - 'foo__lt': '1', - 'bar__gt': '2', - 'baz__gte': '3', - 'bax__lte': '4', - }, + ( + ('foo__lt', '1'), + ('bar__gt', '2'), + ('baz__gte', '3'), + ('bax__lte', '4'), + ), ['"bar" > :p0', '"bax" <= :p1', '"baz" >= :p2', '"foo" < :p3'], [2, 4, 3, 1] ), ( - { - 'foo__like': '2%2', - 'zax__glob': '3*', - }, + ( + ('foo__like', '2%2'), + ('zax__glob', '3*'), + ), ['"foo" like :p0', '"zax" glob :p1'], ['2%2', '3*'] ), + # Multiple like arguments: ( - { - 'foo__isnull': '1', - 'baz__isnull': '1', - 'bar__gt': '10' - }, + ( + ('foo__like', '2%2'), + ('foo__like', '3%3'), + ), + ['"foo" like :p0', '"foo" like :p1'], + ['2%2', '3%3'] + ), + ( + ( + ('foo__isnull', '1'), + ('baz__isnull', '1'), + ('bar__gt', '10'), + ), ['"bar" > :p0', '"baz" is null', '"foo" is null'], [10] ), ( - { - 'foo__in': '1,2,3', - }, + ( + ('foo__in', '1,2,3'), + ), ['foo in (:p0, :p1, :p2)'], ["1", "2", "3"] ), # date ( - { - "foo__date": "1988-01-01", - }, + ( + ("foo__date", "1988-01-01"), + ), ["date(foo) = :p0"], ["1988-01-01"] ), # JSON array variants of __in (useful for unexpected characters) ( - { - 'foo__in': '[1,2,3]', - }, + ( + ('foo__in', '[1,2,3]'), + ), ['foo in (:p0, :p1, :p2)'], [1, 2, 3] ), ( - { - 'foo__in': '["dog,cat", "cat[dog]"]', - }, + ( + ('foo__in', '["dog,cat", "cat[dog]"]'), + ), ['foo in (:p0, :p1)'], ["dog,cat", "cat[dog]"] ), ]) def test_build_where(args, expected_where, expected_params): - f = Filters(sorted(args.items())) + f = Filters(sorted(args)) sql_bits, actual_params = f.build_where_clauses("table") assert expected_where == sql_bits assert {