Merge "Support filtering with units" from #205

pull/205/merge
Simon Willison 2018-04-14 08:12:34 -07:00
commit c857608738
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 17E2DEA2588B7F52
6 zmienionych plików z 115 dodań i 11 usunięć

Wyświetl plik

@ -85,6 +85,12 @@ class BaseView(RenderMixin):
self.page_size = datasette.page_size self.page_size = datasette.page_size
self.max_returned_rows = datasette.max_returned_rows self.max_returned_rows = datasette.max_returned_rows
def table_metadata(self, database, table):
"Fetch table-specific metadata."
return self.ds.metadata.get(
'databases', {}
).get(database, {}).get('tables', {}).get(table, {})
def options(self, request, *args, **kwargs): def options(self, request, *args, **kwargs):
r = response.text('ok') r = response.text('ok')
if self.ds.cors: if self.ds.cors:
@ -450,11 +456,6 @@ class DatabaseDownload(BaseView):
class RowTableShared(BaseView): class RowTableShared(BaseView):
def table_metadata(self, database, table):
return self.ds.metadata.get(
'databases', {}
).get(database, {}).get('tables', {}).get(table, {})
def sortable_columns_for_table(self, name, table, use_rowid): def sortable_columns_for_table(self, name, table, use_rowid):
table_metadata = self.table_metadata(name, table) table_metadata = self.table_metadata(name, table)
if 'sortable_columns' in table_metadata: if 'sortable_columns' in table_metadata:
@ -657,7 +658,9 @@ class TableView(RowTableShared):
forward_querystring=False forward_querystring=False
) )
filters = Filters(sorted(other_args.items())) units = self.table_metadata(name, table).get('units', {})
filters = Filters(sorted(other_args.items()), units, ureg)
where_clauses, params = filters.build_where_clauses() where_clauses, params = filters.build_where_clauses()
# _search support: # _search support:
@ -901,6 +904,7 @@ class TableView(RowTableShared):
'filtered_table_rows_count': filtered_table_rows_count, 'filtered_table_rows_count': filtered_table_rows_count,
'columns': columns, 'columns': columns,
'primary_keys': pks, 'primary_keys': pks,
'units': units,
'query': { 'query': {
'sql': sql, 'sql': sql,
'params': params, 'params': params,
@ -970,6 +974,7 @@ class RowView(RowTableShared):
'columns': columns, 'columns': columns,
'primary_keys': pks, 'primary_keys': pks,
'primary_key_values': pk_values, 'primary_key_values': pk_values,
'units': self.table_metadata(name, table).get('units', {})
} }
if 'foreign_key_tables' in (request.raw_args.get('_extras') or '').split(','): if 'foreign_key_tables' in (request.raw_args.get('_extras') or '').split(','):
@ -1195,6 +1200,11 @@ class Datasette:
} }
return self._inspect return self._inspect
def register_custom_units(self):
"Register any custom units defined in the metadata.json with Pint"
for unit in self.metadata.get('custom_units', []):
ureg.define(unit)
def app(self): def app(self):
app = Sanic(__name__) app = Sanic(__name__)
default_templates = str(app_root / 'datasette' / 'templates') default_templates = str(app_root / 'datasette' / 'templates')
@ -1239,6 +1249,8 @@ class Datasette:
'/<db_name:[^/]+>/<table:[^/]+?>/<pk_path:[^/]+?><as_json:(\.jsono?)?$>' '/<db_name:[^/]+>/<table:[^/]+?>/<pk_path:[^/]+?><as_json:(\.jsono?)?$>'
) )
self.register_custom_units()
@app.exception(Exception) @app.exception(Exception)
def on_exception(request, exception): def on_exception(request, exception):
title = None title = None

Wyświetl plik

@ -160,6 +160,7 @@ def skeleton(files, metadata, sqlite_extensions):
'license_url': None, 'license_url': None,
'source': None, 'source': None,
'source_url': None, 'source_url': None,
'units': {}
} for table_name in (info.get('tables') or {}) } for table_name in (info.get('tables') or {})
} }
} }

Wyświetl plik

@ -10,6 +10,7 @@ import tempfile
import time import time
import shutil import shutil
import urllib import urllib
import numbers
# From https://www.sqlite.org/lang_keywords.html # From https://www.sqlite.org/lang_keywords.html
@ -459,8 +460,10 @@ class Filters:
f.key: f for f in _filters f.key: f for f in _filters
} }
def __init__(self, pairs): def __init__(self, pairs, units={}, ureg=None):
self.pairs = pairs self.pairs = pairs
self.units = units
self.ureg = ureg
def lookups(self): def lookups(self):
"Yields (lookup, display, no_argument) pairs" "Yields (lookup, display, no_argument) pairs"
@ -500,13 +503,27 @@ class Filters:
def has_selections(self): def has_selections(self):
return bool(self.pairs) return bool(self.pairs)
def convert_unit(self, column, value):
"If the user has provided a unit in the quey, convert it into the column unit, if present."
if column not in self.units:
return value
# Try to interpret the value as a unit
value = self.ureg(value)
if isinstance(value, numbers.Number):
# It's just a bare number, assume it's the column unit
return value
column_unit = self.ureg(self.units[column])
return value.to(column_unit).magnitude
def build_where_clauses(self): def build_where_clauses(self):
sql_bits = [] sql_bits = []
params = {} params = {}
for i, (column, lookup, value) in enumerate(self.selections()): for i, (column, lookup, value) in enumerate(self.selections()):
filter = self._filters_by_key.get(lookup, None) filter = self._filters_by_key.get(lookup, None)
if filter: if filter:
sql_bit, param = filter.where_clause(column, value, i) sql_bit, param = filter.where_clause(column, self.convert_unit(column, value), i)
sql_bits.append(sql_bit) sql_bits.append(sql_bit)
if param is not None: if param is not None:
param_id = 'p{}'.format(i) param_id = 'p{}'.format(i)

Wyświetl plik

@ -55,6 +55,43 @@ You can also provide metadata at the per-database or per-table level, like this:
Each of the top-level metadata fields can be used at the database and table level. Each of the top-level metadata fields can be used at the database and table level.
Specifying units for a column
-----------------------------
Datasette supports attaching units to a column, which will be used when displaying
values from that column. SI prefixes will be used where appropriate.
Column units are configured in the metadata like so::
{
"databases": {
"database1": {
"tables": {
"example_table": {
"units": {
"column1": "metres",
"column2": "Hz"
}
}
}
}
}
}
Units are interpreted using Pint_, and you can see the full list of available units in
Pint's `unit registry`_. You can also add `custom units`_ to the metadata, which will be
registered with Pint::
{
"custom_units": [
"decibel = [] = dB"
]
}
.. _Pint: https://pint.readthedocs.io/
.. _unit registry: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
.. _custom units: http://pint.readthedocs.io/en/latest/defining.html
Setting which columns can be used for sorting Setting which columns can be used for sorting
--------------------------------------------- ---------------------------------------------
@ -119,7 +156,8 @@ This will create a ``metadata.json`` file looking something like this::
"license": null, "license": null,
"license_url": null, "license_url": null,
"source": null, "source": null,
"source_url": null "source_url": null,
"units": {}
} }
} }
}, },

Wyświetl plik

@ -79,6 +79,12 @@ METADATA = {
'no_primary_key': { 'no_primary_key': {
'sortable_columns': [], 'sortable_columns': [],
}, },
'units': {
'units': {
'distance': 'm',
'frequency': 'Hz'
}
},
} }
}, },
} }
@ -169,6 +175,16 @@ CREATE TABLE "complex_foreign_keys" (
FOREIGN KEY ("f3") REFERENCES [simple_primary_key](id) FOREIGN KEY ("f3") REFERENCES [simple_primary_key](id)
); );
CREATE TABLE units (
pk integer primary key,
distance int,
frequency int
);
INSERT INTO units VALUES (1, 1, 100);
INSERT INTO units VALUES (2, 5000, 2500);
INSERT INTO units VALUES (3, 100000, 75000);
CREATE TABLE [select] ( CREATE TABLE [select] (
[group] text, [group] text,
[having] text, [having] text,
@ -207,7 +223,6 @@ CREATE VIEW simple_view AS
).replace('None', 'null') for row in generate_sortable_rows(201) ).replace('None', 'null') for row in generate_sortable_rows(201)
]) ])
if __name__ == '__main__': if __name__ == '__main__':
filename = sys.argv[-1] filename = sys.argv[-1]
if filename.endswith('.db'): if filename.endswith('.db'):

Wyświetl plik

@ -14,7 +14,7 @@ def test_homepage(app_client):
assert response.json.keys() == {'test_tables': 0}.keys() assert response.json.keys() == {'test_tables': 0}.keys()
d = response.json['test_tables'] d = response.json['test_tables']
assert d['name'] == 'test_tables' assert d['name'] == 'test_tables'
assert d['tables_count'] == 12 assert d['tables_count'] == 13
def test_database_page(app_client): def test_database_page(app_client):
@ -172,6 +172,14 @@ def test_database_page(app_client):
'foreign_keys': {'incoming': [], 'outgoing': []}, 'foreign_keys': {'incoming': [], 'outgoing': []},
'label_column': None, 'label_column': None,
'primary_keys': ['pk'], 'primary_keys': ['pk'],
}, {
'columns': ['pk', 'distance', 'frequency'],
'name': 'units',
'count': 3,
'hidden': False,
'foreign_keys': {'incoming': [], 'outgoing': []},
'label_column': None,
'primary_keys': ['pk'],
}] == data['tables'] }] == data['tables']
@ -577,3 +585,16 @@ def test_row_foreign_key_tables(app_client):
'other_column': 'f1', 'other_column': 'f1',
'other_table': 'complex_foreign_keys' 'other_table': 'complex_foreign_keys'
}] == response.json['foreign_key_tables'] }] == response.json['foreign_key_tables']
def test_unit_filters(app_client):
response = app_client.get('/test_tables/units.json?distance__lt=75km&frequency__gt=1kHz',
gather_request=False)
assert response.status == 200
data = response.json
assert data['units']['distance'] == 'm'
assert data['units']['frequency'] == 'Hz'
assert len(data['rows']) == 1
assert data['rows'][0][0] == 2