Row pages link to foreign keys from table display, closes #1592

https://gisthost.github.io/?40813f5b3e4d83c0efe1c09135f84290/index.html

Also now shows primary key column first and in bold on that page.
This commit is contained in:
Simon Willison 2026-03-06 20:16:50 -08:00
commit 97201f067c
2 changed files with 88 additions and 8 deletions

View file

@ -5,12 +5,14 @@ from datasette.resources import TableResource
from .base import DataView, BaseView, _error
from datasette.utils import (
await_me_maybe,
CustomRow,
make_slot_function,
to_css_class,
escape_sqlite,
)
from datasette.plugins import pm
import json
import markupsafe
import sqlite_utils
from .table import display_columns_and_rows, _get_extras
@ -42,13 +44,62 @@ class RowView(DataView):
if not rows:
raise NotFound(f"Record not found: {pk_values}")
pks = resolved.pks
async def template_data():
# Reorder columns so primary keys come first
pk_set = set(pks)
pk_cols = [d for d in results.description if d[0] in pk_set]
non_pk_cols = [d for d in results.description if d[0] not in pk_set]
reordered_description = pk_cols + non_pk_cols
reordered_columns = [d[0] for d in reordered_description]
# Reorder row data to match
reordered_rows = []
for row in rows:
new_row = CustomRow(reordered_columns)
for col in reordered_columns:
new_row[col] = row[col]
reordered_rows.append(new_row)
# Expand foreign key columns into dicts so display_columns_and_rows
# renders them as hyperlinks, matching the table view behavior
expanded_rows = reordered_rows
for fk in await db.foreign_keys_for_table(table):
column = fk["column"]
if column not in reordered_columns:
continue
column_index = reordered_columns.index(column)
values = [row[column_index] for row in expanded_rows]
expanded_labels = await self.ds.expand_foreign_keys(
request.actor, database, table, column, values
)
if expanded_labels:
new_rows = []
for row in expanded_rows:
new_row = CustomRow(reordered_columns)
for col in reordered_columns:
value = row[col]
if (
col == column
and (col, value) in expanded_labels
and value is not None
):
new_row[col] = {
"value": value,
"label": expanded_labels[(col, value)],
}
else:
new_row[col] = value
new_rows.append(new_row)
expanded_rows = new_rows
display_columns, display_rows = await display_columns_and_rows(
self.ds,
database,
table,
results.description,
rows,
reordered_description,
expanded_rows,
link_column=False,
truncate_cells=0,
request=request,
@ -56,6 +107,14 @@ class RowView(DataView):
for column in display_columns:
column["sortable"] = False
# Bold primary key cell values
for row in display_rows:
for cell in row:
if cell["column"] in pk_set:
cell["value"] = markupsafe.Markup(
"<strong>{}</strong>".format(cell["value"])
)
row_actions = []
for hook in pm.hook.row_actions(
datasette=self.ds,
@ -71,6 +130,7 @@ class RowView(DataView):
return {
"private": private,
"columns": reordered_columns,
"foreign_key_tables": await self.foreign_key_tables(
database, table, pk_values
),

View file

@ -347,7 +347,7 @@ async def test_row_html_simple_primary_key(ds_client):
assert ["id", "content"] == [th.string.strip() for th in table.select("thead th")]
assert [
[
'<td class="col-id type-int">1</td>',
'<td class="col-id type-int"><strong>1</strong></td>',
'<td class="col-content type-str">hello</td>',
]
] == [[str(td) for td in tr.select("td")] for tr in table.select("tbody tr")]
@ -363,7 +363,7 @@ async def test_row_html_no_primary_key(ds_client):
]
expected = [
[
'<td class="col-rowid type-int">1</td>',
'<td class="col-rowid type-int"><strong>1</strong></td>',
'<td class="col-content type-str">1</td>',
'<td class="col-a type-str">a1</td>',
'<td class="col-b type-str">b1</td>',
@ -406,6 +406,26 @@ async def test_row_links_from_other_tables(
assert link == expected_link
@pytest.mark.asyncio
async def test_row_foreign_key_links(ds_client):
# Row detail page should render foreign key values as hyperlinks
response = await ds_client.get("/fixtures/foreign_key_references/1")
assert response.status_code == 200
soup = Soup(response.text, "html.parser")
# foreign_key_with_label=1 references simple_primary_key(id=1, content="hello")
td = soup.find("td", {"class": "col-foreign_key_with_label"})
a = td.find("a")
assert a is not None, "Expected foreign key value to be a hyperlink"
assert a["href"] == "/fixtures/simple_primary_key/1"
assert a.text == "hello"
# Primary key column should be first and bold
table = soup.find("table")
headers = [th.text.strip() for th in table.select("thead th")]
assert headers[0] == "pk"
first_td = table.select("tbody tr td")[0]
assert first_td.find("strong") is not None, "PK value should be bold"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"path,expected",
@ -414,8 +434,8 @@ async def test_row_links_from_other_tables(
"/fixtures/compound_primary_key/a,b",
[
[
'<td class="col-pk1 type-str">a</td>',
'<td class="col-pk2 type-str">b</td>',
'<td class="col-pk1 type-str"><strong>a</strong></td>',
'<td class="col-pk2 type-str"><strong>b</strong></td>',
'<td class="col-content type-str">c</td>',
]
],
@ -424,8 +444,8 @@ async def test_row_links_from_other_tables(
"/fixtures/compound_primary_key/a~2Fb,~2Ec~2Dd",
[
[
'<td class="col-pk1 type-str">a/b</td>',
'<td class="col-pk2 type-str">.c-d</td>',
'<td class="col-pk1 type-str"><strong>a/b</strong></td>',
'<td class="col-pk2 type-str"><strong>.c-d</strong></td>',
'<td class="col-content type-str">c</td>',
]
],