This commit is contained in:
NekoPaw 2026-10-14 19:14:04 +08:00 • committed by GitHub
commit e42a981d5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 90 additions and 5 deletions

View file

@ -721,6 +721,8 @@ async def display_columns_and_rows(
# Unless we are a view, the first column is a link - either to the rowid
# or to the simple or compound primary key
if link_column:
# NULL keys may be duplicated and encode like a real string key.
has_null_pk = bool(pks) and any(row[pk] is None for pk in pks)
is_special_link_column = len(pks) != 1
pk_path = path_from_row_pks(row, pks, not pks, False)
row_path = path_from_row_pks(row, pks, not pks)
@ -729,9 +731,9 @@ async def display_columns_and_rows(
if row_label and row_label != pk_path:
row_action_label = f"{pk_path} {row_label}"
table_path = datasette.urls.table(database_name, table_name)
row_link = (
f'<a href="{table_path}/{row_path}">{markupsafe.escape(pk_path)!s}</a>'
)
row_link = markupsafe.escape(pk_path)
if not has_null_pk:
row_link = f'<a href="{table_path}/{row_path}">{markupsafe.escape(pk_path)!s}</a>'
edit_icon = (
'<svg class="row-inline-action-icon" aria-hidden="true" '
'xmlns="http://www.w3.org/2000/svg" width="14" height="14" '
@ -754,14 +756,14 @@ async def display_columns_and_rows(
"</svg>"
)
row_actions = []
if row_action_permissions.get("update-row"):
if not has_null_pk and row_action_permissions.get("update-row"):
row_actions.append(
'<button type="button" class="row-inline-action row-inline-action-edit" '
f'aria-label="Edit row {markupsafe.escape(row_action_label)}" title="Edit row" '
'data-row-action="edit">'
f"{edit_icon}</button>"
)
if row_action_permissions.get("delete-row"):
if not has_null_pk and row_action_permissions.get("delete-row"):
row_actions.append(
'<button type="button" class="row-inline-action row-inline-action-delete" '
f'aria-label="Delete row {markupsafe.escape(row_action_label)}" title="Delete row" '

View file

@ -1087,6 +1087,89 @@ async def test_row_delete_action_data_attributes():
ds.close()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"schema_and_rows, link_column, valid_row_path",
[
(
"""
create table items (id text primary key, name text);
insert into items (id, name) values
(null, 'Null key'),
('None', 'Literal key');
""",
"id",
"/data/items/None",
),
(
"""
create table items (
namespace text,
id text,
name text,
primary key (namespace, id)
);
insert into items (namespace, id, name) values
(null, 'item', 'Null key'),
('None', 'item', 'Literal key');
""",
"Link",
"/data/items/None,item",
),
],
)
async def test_null_primary_key_rows_have_no_row_link_or_actions(
schema_and_rows, link_column, valid_row_path
):
ds = Datasette(
[],
config={
"databases": {
"data": {
"tables": {
"items": {
"permissions": {
"update-row": {"id": "root"},
"delete-row": {"id": "root"},
},
},
},
},
},
},
)
try:
db = ds.add_database(
Database(ds, memory_name="test_null_primary_key_rows"), name="data"
)
await db.execute_write_script(schema_and_rows)
response = await ds.client.get("/data/items", actor={"id": "root"})
assert response.status_code == 200
soup = Soup(response.text, "html.parser")
rows = soup.select("table.rows-and-columns tbody tr")
null_row = next(
row
for row in rows
if row.select_one(".col-name").get_text(strip=True) == "Null key"
)
literal_row = next(
row
for row in rows
if row.select_one(".col-name").get_text(strip=True) == "Literal key"
)
assert null_row.select_one(f'td[class~="col-{link_column}"] a') is None
assert null_row.select("button[data-row-action]") == []
literal_link = literal_row.select_one(f'td[class~="col-{link_column}"] a')
assert literal_link is not None
assert literal_link["href"] == valid_row_path
assert len(literal_row.select("button[data-row-action]")) == 2
finally:
ds.close()
@pytest.mark.asyncio
async def test_database_create_table_action_button_and_data():
ds = Datasette(