kopia lustrzana https://github.com/simonw/datasette
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
rodzic
a0acc934f7
commit
2fa60bc5e3
107
datasette/app.py
107
datasette/app.py
|
@ -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(' ')
|
display_value = jinja2.Markup(' ')
|
||||||
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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Ładowanie…
Reference in New Issue