Table views now show expanded foreign key references, if possible

If a table has foreign key columns, and those foreign key tables have
label_columns, the TableView will now query those other tables for the
corresponding values and display those values as links in the corresponding
table cells.

label_columns are currently detected by the inspect() function, which looks
for any table that has just two columns - an ID column and one other - and
sets the label_column to be that second non-ID column.
pull/118/head
Simon Willison 2017-11-17 19:09:32 -08:00
rodzic a0acc934f7
commit 2fa60bc5e3
2 zmienionych plików z 126 dodań i 54 usunięć

Wyświetl plik

@ -221,16 +221,23 @@ class BaseView(HTTPMethodView):
headers=headers, headers=headers,
) )
else: else:
context = {**data, **dict( extras = {}
extra_template_data() if callable(extra_template_data):
if callable(extra_template_data) extras = extra_template_data()
else extra_template_data if asyncio.iscoroutine(extras):
), **{ extras = await extras
else:
extras = extra_template_data
context = {
**data,
**extras,
**{
'url_json': path_with_ext(request, '.json'), 'url_json': path_with_ext(request, '.json'),
'url_jsono': path_with_ext(request, '.jsono'), 'url_jsono': path_with_ext(request, '.jsono'),
'metadata': self.ds.metadata, 'metadata': self.ds.metadata,
'datasette_version': __version__, 'datasette_version': __version__,
}} }
}
r = self.jinja.render( r = self.jinja.render(
template, template,
request, request,
@ -481,6 +488,8 @@ class TableView(BaseView):
table_rows = None table_rows = None
if not is_view: if not is_view:
table_rows = info[name]['tables'][table]['count'] table_rows = info[name]['tables'][table]['count']
# Pagination next link
next_value = None next_value = None
next_url = None next_url = None
if len(rows) > self.page_size: if len(rows) > self.page_size:
@ -492,6 +501,14 @@ class TableView(BaseView):
'_next': next_value, '_next': next_value,
})) }))
async def extra_template():
return {
'database_hash': hash,
'use_rowid': use_rowid,
'display_columns': display_columns,
'display_rows': await self.make_display_rows(name, hash, table, rows, display_columns, pks, is_view, use_rowid),
}
return { return {
'database': name, 'database': name,
'table': table, 'table': table,
@ -509,15 +526,37 @@ class TableView(BaseView):
}, },
'next': next_value and str(next_value) or None, 'next': next_value and str(next_value) or None,
'next_url': next_url, 'next_url': next_url,
}, lambda: { }, extra_template
'database_hash': hash,
'use_rowid': use_rowid,
'display_columns': display_columns,
'display_rows': make_display_rows(name, hash, table, rows, display_columns, pks, is_view, use_rowid),
}
async def make_display_rows(self, database, database_hash, table, rows, display_columns, pks, is_view, use_rowid):
# Get fancy with foreign keys
expanded = {}
tables = self.ds.inspect()[database]['tables']
table_info = tables.get(table) or {}
if table_info:
foreign_keys = table_info['foreign_keys']['outgoing']
for fk in foreign_keys:
label_column = tables.get(fk['other_table'], {}).get('label_column')
if not label_column:
# We only link cells to other tables with label columns defined
continue
ids_to_lookup = set([row[fk['column']] for row in rows])
sql = 'select "{other_column}", "{label_column}" from {other_table} where "{other_column}" in ({placeholders})'.format(
other_column=fk['other_column'],
label_column=label_column,
other_table=escape_sqlite_table_name(fk['other_table']),
placeholders=', '.join(['?'] * len(ids_to_lookup)),
)
try:
results = await self.execute(database, sql, list(set(ids_to_lookup)))
except sqlite3.OperationalError:
# Probably hit the timelimit
pass
else:
for id, value in results:
expanded[(fk['column'], id)] = (fk['other_table'], value)
def make_display_rows(database, database_hash, table, rows, display_columns, pks, is_view, use_rowid): to_return = []
for row in rows: for row in rows:
cells = [] cells = []
# Unless we are a view, the first column is a link - either to the rowid # Unless we are a view, the first column is a link - either to the rowid
@ -540,8 +579,18 @@ def make_display_rows(database, database_hash, table, rows, display_columns, pks
if use_rowid and column == 'rowid': if use_rowid and column == 'rowid':
# We already showed this in the linked first column # We already showed this in the linked first column
continue continue
if False: # TODO: This is where we will do foreign key linking elif (column, value) in expanded:
display_value = jinja2.Markup('<a href="#">{}</a>'.format('foreign key')) other_table, label = expanded[(column, value)]
display_value = jinja2.Markup(
# TODO: Escape id/label/etc so no XSS here
'<a href="/{database}-{database_hash}/{table}/{id}">{label}</a>'.format(
database=database,
database_hash=database_hash,
table=escape_sqlite_table_name(other_table),
id=value,
label=label,
)
)
elif value is None: elif value is None:
display_value = jinja2.Markup('&nbsp;') display_value = jinja2.Markup('&nbsp;')
else: else:
@ -550,7 +599,8 @@ def make_display_rows(database, database_hash, table, rows, display_columns, pks
'column': column, 'column': column,
'value': display_value, 'value': display_value,
}) })
yield cells to_return.append(cells)
return to_return
class RowView(BaseView): class RowView(BaseView):
@ -581,6 +631,13 @@ class RowView(BaseView):
rows = list(rows) rows = list(rows)
if not rows: if not rows:
raise NotFound('Record not found: {}'.format(pk_values)) raise NotFound('Record not found: {}'.format(pk_values))
async def template_data():
return {
'database_hash': hash,
'foreign_key_tables': await self.foreign_key_tables(name, table, pk_values),
}
return { return {
'database': name, 'database': name,
'table': table, 'table': table,
@ -588,10 +645,7 @@ class RowView(BaseView):
'columns': columns, 'columns': columns,
'primary_keys': pks, 'primary_keys': pks,
'primary_key_values': pk_values, 'primary_key_values': pk_values,
}, { }, template_data
'database_hash': hash,
'foreign_key_tables': await self.foreign_key_tables(name, table, pk_values),
}
async def foreign_key_tables(self, name, table, pk_values): async def foreign_key_tables(self, name, table, pk_values):
if len(pk_values) != 1: if len(pk_values) != 1:
@ -666,8 +720,19 @@ class Datasette:
for r in conn.execute('select * from sqlite_master where type="table"') for r in conn.execute('select * from sqlite_master where type="table"')
] ]
for table in table_names: for table in table_names:
count = conn.execute(
'select count(*) from {}'.format(escape_sqlite_table_name(table))
).fetchone()[0]
label_column = None
# If table has two columns, one of which is ID, then label_column is the other one
column_names = [r[1] for r in conn.execute(
'PRAGMA table_info({});'.format(escape_sqlite_table_name(table))
).fetchall()]
if column_names and len(column_names) == 2 and 'id' in column_names:
label_column = [c for c in column_names if c != 'id'][0]
tables[table] = { tables[table] = {
'count': conn.execute('select count(*) from "{}"'.format(table)).fetchone()[0], 'count': count,
'label_column': label_column,
} }
foreign_keys = get_all_foreign_keys(conn) foreign_keys = get_all_foreign_keys(conn)

Wyświetl plik

@ -21,6 +21,13 @@ td {
th { th {
padding-right: 1em; padding-right: 1em;
} }
table a:link {
text-decoration: none;
color: #445ac8;
}
table a:visited {
color: #8f54c4;
}
@media only screen and (max-width: 576px) { @media only screen and (max-width: 576px) {
/* Force table to not be like tables anymore */ /* Force table to not be like tables anymore */
table, thead, tbody, th, td, tr { table, thead, tbody, th, td, tr {