Foreign key facets are now expanded to labels, refs #255

This commit is contained in:
Simon Willison 2018-05-15 10:52:02 -05:00 committed by Simon Willison
commit 6d12580ed7
6 changed files with 162 additions and 59 deletions

View file

@ -102,7 +102,7 @@
<p><strong><a href="{{ path_with_removed_args(request, {'_facet': facet_name}) }}">{{ facet_name }}</a></strong></p>
<ul>
{% for facet_value in facet_info.results %}
<li><a href="{{ facet_value.toggle_url }}">{{ facet_value.value }}</a> {{ "{:,}".format(facet_value.count) }}</li>
<li><a href="{{ facet_value.toggle_url }}">{{ facet_value.label }}</a> {{ "{:,}".format(facet_value.count) }}</li>
{% endfor %}
{% if facet_info.truncated %}
<li>...</li>

View file

@ -35,6 +35,54 @@ class RowTableShared(BaseView):
sortable_columns.add("rowid")
return sortable_columns
async def expand_foreign_keys(self, database, table, column, values):
"Returns dict mapping (column, value) -> label"
labeled_fks = {}
tables_info = self.ds.inspect()[database]["tables"]
table_info = tables_info.get(table) or {}
if not table_info:
return {}
foreign_keys = table_info["foreign_keys"]["outgoing"]
# Find the foreign_key for this column
try:
fk = [
foreign_key for foreign_key in foreign_keys
if foreign_key["column"] == column
][0]
except IndexError:
return {}
label_column = (
# First look in metadata.json for this foreign key table:
self.table_metadata(
database, fk["other_table"]
).get("label_column")
or tables_info.get(fk["other_table"], {}).get("label_column")
)
if not label_column:
return {}
labeled_fks = {}
sql = '''
select {other_column}, {label_column}
from {other_table}
where {other_column} in ({placeholders})
'''.format(
other_column=escape_sqlite(fk["other_column"]),
label_column=escape_sqlite(label_column),
other_table=escape_sqlite(fk["other_table"]),
placeholders=", ".join(["?"] * len(set(values))),
)
try:
results = await self.execute(
database, sql, list(set(values))
)
except sqlite3.OperationalError:
# Probably hit the timelimit
pass
else:
for id, value in results:
labeled_fks[(fk["column"], id)] = value
return labeled_fks
async def display_columns_and_rows(
self,
database,
@ -514,7 +562,11 @@ class TableView(RowTableShared):
"results": [],
"truncated": len(facet_rows) > FACET_SIZE,
}
for row in facet_rows[:FACET_SIZE]:
facet_rows = facet_rows[:FACET_SIZE]
# Attempt to expand foreign keys into labels
values = [row["value"] for row in facet_rows]
expanded = (await self.expand_foreign_keys(name, table, column, values))
for row in facet_rows:
selected = str(other_args.get(column)) == str(row["value"])
if selected:
toggle_path = path_with_removed_args(
@ -526,6 +578,10 @@ class TableView(RowTableShared):
)
facet_results[column]["results"].append({
"value": row["value"],
"label": expanded.get(
(column, row["value"]),
row["value"]
),
"count": row["count"],
"toggle_url": urllib.parse.urljoin(
request.url, toggle_path

View file

@ -267,29 +267,41 @@ CREATE TABLE [select] (
);
INSERT INTO [select] VALUES ('group', 'having', 'and');
CREATE TABLE facet_cities (
id integer primary key,
name text
);
INSERT INTO facet_cities (id, name) VALUES
(1, 'San Francisco'),
(2, 'Los Angeles'),
(3, 'Detroit'),
(4, 'Memnonia')
;
CREATE TABLE facetable (
pk integer primary key,
planet_id integer,
planet_int integer,
state text,
city text,
neighborhood text
city_id integer,
neighborhood text,
FOREIGN KEY ("city_id") REFERENCES [facet_cities](id)
);
INSERT INTO facetable (planet_id, state, city, neighborhood) VALUES
(1, 'CA', 'San Francisco', 'Mission'),
(1, 'CA', 'San Francisco', 'Dogpatch'),
(1, 'CA', 'San Francisco', 'SOMA'),
(1, 'CA', 'San Francisco', 'Tenderloin'),
(1, 'CA', 'San Francisco', 'Bernal Heights'),
(1, 'CA', 'San Francisco', 'Hayes Valley'),
(1, 'CA', 'Los Angeles', 'Hollywood'),
(1, 'CA', 'Los Angeles', 'Downtown'),
(1, 'CA', 'Los Angeles', 'Los Feliz'),
(1, 'CA', 'Los Angeles', 'Koreatown'),
(1, 'MI', 'Detroit', 'Downtown'),
(1, 'MI', 'Detroit', 'Greektown'),
(1, 'MI', 'Detroit', 'Corktown'),
(1, 'MI', 'Detroit', 'Mexicantown'),
(2, 'MC', 'Memnonia', 'Arcadia Planitia')
INSERT INTO facetable (planet_int, state, city_id, neighborhood) VALUES
(1, 'CA', 1, 'Mission'),
(1, 'CA', 1, 'Dogpatch'),
(1, 'CA', 1, 'SOMA'),
(1, 'CA', 1, 'Tenderloin'),
(1, 'CA', 1, 'Bernal Heights'),
(1, 'CA', 1, 'Hayes Valley'),
(1, 'CA', 2, 'Hollywood'),
(1, 'CA', 2, 'Downtown'),
(1, 'CA', 2, 'Los Feliz'),
(1, 'CA', 2, 'Koreatown'),
(1, 'MI', 3, 'Downtown'),
(1, 'MI', 3, 'Greektown'),
(1, 'MI', 3, 'Corktown'),
(1, 'MI', 3, 'Mexicantown'),
(2, 'MC', 4, 'Arcadia Planitia')
;
INSERT INTO simple_primary_key VALUES (1, 'hello');

View file

@ -19,7 +19,7 @@ def test_homepage(app_client):
assert response.json.keys() == {'test_tables': 0}.keys()
d = response.json['test_tables']
assert d['name'] == 'test_tables'
assert d['tables_count'] == 16
assert d['tables_count'] == 17
def test_database_page(app_client):
@ -103,10 +103,33 @@ def test_database_page(app_client):
'fts_table': None,
'primary_keys': ['pk'],
}, {
'columns': ['pk', 'planet_id', 'state', 'city', 'neighborhood'],
'columns': ['id', 'name'],
'name': 'facet_cities',
'count': 4,
'foreign_keys': {
'incoming': [{
'column': 'id',
'other_column': 'city_id',
'other_table': 'facetable',
}],
'outgoing': []
},
'fts_table': None,
'hidden': False,
'label_column': 'name',
'primary_keys': ['id'],
}, {
'columns': ['pk', 'planet_int', 'state', 'city_id', 'neighborhood'],
'name': 'facetable',
'count': 15,
'foreign_keys': {'incoming': [], 'outgoing': []},
'foreign_keys': {
'incoming': [],
'outgoing': [{
'column': 'city_id',
'other_column': 'id',
'other_table': 'facet_cities'
}],
},
'fts_table': None,
'hidden': False,
'label_column': None,
@ -889,57 +912,64 @@ def test_page_size_matching_max_returned_rows(app_client_returend_rows_matches_p
@pytest.mark.parametrize('path,expected_facet_results', [
(
"/test_tables/facetable.json?_facet=state&_facet=city",
"/test_tables/facetable.json?_facet=state&_facet=city_id",
{
"state": {
"name": "state",
"results": [
{
"value": "CA",
"label": "CA",
"count": 10,
"toggle_url": "_facet=state&_facet=city&state=CA",
"toggle_url": "_facet=state&_facet=city_id&state=CA",
"selected": False,
},
{
"value": "MI",
"label": "MI",
"count": 4,
"toggle_url": "_facet=state&_facet=city&state=MI",
"toggle_url": "_facet=state&_facet=city_id&state=MI",
"selected": False,
},
{
"value": "MC",
"label": "MC",
"count": 1,
"toggle_url": "_facet=state&_facet=city&state=MC",
"toggle_url": "_facet=state&_facet=city_id&state=MC",
"selected": False,
}
],
"truncated": False,
},
"city": {
"name": "city",
"city_id": {
"name": "city_id",
"results": [
{
"value": "San Francisco",
"value": 1,
"label": "San Francisco",
"count": 6,
"toggle_url": "_facet=state&_facet=city&city=San+Francisco",
"toggle_url": "_facet=state&_facet=city_id&city_id=1",
"selected": False,
},
{
"value": "Detroit",
"value": 2,
"label": "Los Angeles",
"count": 4,
"toggle_url": "_facet=state&_facet=city&city=Detroit",
"toggle_url": "_facet=state&_facet=city_id&city_id=2",
"selected": False,
},
{
"value": "Los Angeles",
"value": 3,
"label": "Detroit",
"count": 4,
"toggle_url": "_facet=state&_facet=city&city=Los+Angeles",
"toggle_url": "_facet=state&_facet=city_id&city_id=3",
"selected": False,
},
{
"value": "Memnonia",
"value": 4,
"label": "Memnonia",
"count": 1,
"toggle_url": "_facet=state&_facet=city&city=Memnonia",
"toggle_url": "_facet=state&_facet=city_id&city_id=4",
"selected": False,
}
],
@ -947,67 +977,72 @@ def test_page_size_matching_max_returned_rows(app_client_returend_rows_matches_p
}
}
), (
"/test_tables/facetable.json?_facet=state&_facet=city&state=MI",
"/test_tables/facetable.json?_facet=state&_facet=city_id&state=MI",
{
"state": {
"name": "state",
"results": [
{
"value": "MI",
"label": "MI",
"count": 4,
"selected": True,
"toggle_url": "_facet=state&_facet=city",
"toggle_url": "_facet=state&_facet=city_id",
},
],
"truncated": False,
},
"city": {
"name": "city",
"city_id": {
"name": "city_id",
"results": [
{
"value": "Detroit",
"value": 3,
"label": "Detroit",
"count": 4,
"selected": False,
"toggle_url": "_facet=state&_facet=city&state=MI&city=Detroit",
"toggle_url": "_facet=state&_facet=city_id&state=MI&city_id=3",
},
],
"truncated": False,
},
},
), (
"/test_tables/facetable.json?_facet=planet_id",
"/test_tables/facetable.json?_facet=planet_int",
{
"planet_id": {
"name": "planet_id",
"planet_int": {
"name": "planet_int",
"results": [
{
"value": 1,
"label": 1,
"count": 14,
"selected": False,
"toggle_url": "_facet=planet_id&planet_id=1",
"toggle_url": "_facet=planet_int&planet_int=1",
},
{
"value": 2,
"label": 2,
"count": 1,
"selected": False,
"toggle_url": "_facet=planet_id&planet_id=2",
"toggle_url": "_facet=planet_int&planet_int=2",
},
],
"truncated": False,
}
},
), (
# planet_id is an integer field:
"/test_tables/facetable.json?_facet=planet_id&planet_id=1",
# planet_int is an integer field:
"/test_tables/facetable.json?_facet=planet_int&planet_int=1",
{
"planet_id": {
"name": "planet_id",
"planet_int": {
"name": "planet_int",
"results": [
{
"value": 1,
"label": 1,
"count": 14,
"selected": True,
"toggle_url": "_facet=planet_id",
"toggle_url": "_facet=planet_int",
}
],
"truncated": False,

View file

@ -203,15 +203,15 @@ def test_sort_links(app_client):
def test_facets_persist_through_filter_form(app_client):
response = app_client.get(
'/test_tables/facetable?_facet=planet_id&_facet=city',
'/test_tables/facetable?_facet=planet_int&_facet=city_id',
gather_request=False
)
assert response.status == 200
inputs = Soup(response.body, 'html.parser').find('form').findAll('input')
hiddens = [i for i in inputs if i['type'] == 'hidden']
assert [
('_facet', 'planet_id'),
('_facet', 'city'),
('_facet', 'planet_int'),
('_facet', 'city_id'),
] == [
(hidden['name'], hidden['value']) for hidden in hiddens
]

View file

@ -36,8 +36,8 @@ def test_urlsafe_components(path, expected):
('city', 'Detroit'),
), '/?_facet=state&_facet=city&state=MI&city=Detroit'),
('/?_facet=state&_facet=city', (
('_facet', 'planet_id'),
), '/?_facet=state&_facet=city&_facet=planet_id'),
('_facet', 'planet_int'),
), '/?_facet=state&_facet=city&_facet=planet_int'),
])
def test_path_with_added_args(path, added_args, expected):
request = Request(