This commit is contained in:
Sebastian Cao 2026-10-13 01:39:24 +08:00 • committed by GitHub
commit f55764fcd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 102 additions and 2 deletions

View file

@ -5,6 +5,7 @@ import markupsafe
from datasette import hookimpl
from datasette.column_types import ColumnType, SQLiteType
from datasette.utils import truncate_url
_HTTP_URL_RE = re.compile(r"https?://\S+", re.IGNORECASE)
@ -23,14 +24,17 @@ class UrlColumnType(ColumnType):
description = "URL"
sqlite_types = (SQLiteType.TEXT,)
async def render_cell(self, value, column, table, database, datasette, request):
async def render_cell(
self, value, column, table, database, datasette, request, truncate_cells=0
):
if not value or not isinstance(value, str):
return None
normalized = _normalize_http_url(value)
if normalized is None:
return markupsafe.escape(value.strip())
escaped = markupsafe.escape(normalized)
return markupsafe.Markup(f'<a href="{escaped}">{escaped}</a>')
link_text = markupsafe.escape(truncate_url(normalized, truncate_cells))
return markupsafe.Markup(f'<a href="{escaped}">{link_text}</a>')
async def validate(self, value, datasette):
if value is None or value == "":

View file

@ -1,4 +1,5 @@
import asyncio
import inspect
import itertools
import json
import time
@ -666,6 +667,13 @@ async def display_columns_and_rows(
# Look up column types for this table
column_types_map = await datasette.get_column_types(database_name, table_name)
# Column types can opt in to this page's truncation length by accepting
# a truncate_cells argument to their render_cell() method
truncate_cells_columns = {
column
for column, ct in column_types_map.items()
if "truncate_cells" in inspect.signature(ct.render_cell).parameters
}
column_details = {
col.name: col for col in await db.table_column_details(table_name)
@ -796,6 +804,9 @@ async def display_columns_and_rows(
plugin_display_value = None
ct = column_types_map.get(column)
if ct:
kwargs = {}
if column in truncate_cells_columns:
kwargs["truncate_cells"] = truncate_cells
candidate = await ct.render_cell(
value=value,
column=column,
@ -803,6 +814,7 @@ async def display_columns_and_rows(
database=database_name,
datasette=datasette,
request=request,
**kwargs,
)
if candidate is not None:
plugin_display_value = candidate

View file

@ -914,6 +914,90 @@ async def test_html_table_page_rendering(ds_ct):
assert 'href="https://example.com"' in html
# --- url column type link text truncation, refs #1805 ---
LONG_URL = (
"https://images.openfoodfacts.org/images/products/000/000/000/088/"
"nutrition_fr.5.200.jpg"
)
@pytest.fixture
def ds_long_url(tmp_path_factory):
db_path = str(tmp_path_factory.mktemp("dbs") / "data.db")
db = sqlite3.connect(db_path)
db.execute("create table posts (id integer primary key, website text)")
db.execute("insert into posts values (1, ?)", [LONG_URL])
db.execute("insert into posts values (2, 'not a url')")
db.commit()
db.close()
ds = Datasette(
[db_path],
settings={"truncate_cells_html": 30},
config={
"databases": {
"data": {"tables": {"posts": {"column_types": {"website": "url"}}}}
}
},
)
yield ds
ds.close()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"truncate_cells_html,expected_text",
(
(30, "https://images.openfoodfac….jpg"),
# 0 disables truncation
(0, LONG_URL),
),
)
async def test_url_column_type_truncates_link_text(
ds_long_url, truncate_cells_html, expected_text
):
# The table page truncates the link text but the href keeps the full URL
ds_long_url._settings["truncate_cells_html"] = truncate_cells_html
await ds_long_url.invoke_startup()
response = await ds_long_url.client.get("/data/posts")
assert response.status_code == 200
link = Soup(response.text, "html.parser").select_one("td.col-website a")
assert link["href"] == LONG_URL
assert link.text == expected_text
@pytest.mark.asyncio
async def test_url_column_type_full_link_text_on_row_page(ds_long_url):
# Row pages show full values, so the link text is not truncated there
await ds_long_url.invoke_startup()
response = await ds_long_url.client.get("/data/posts/1")
assert response.status_code == 200
link = Soup(response.text, "html.parser").select_one("td.col-website a")
assert link["href"] == LONG_URL
assert link.text == LONG_URL
@pytest.mark.asyncio
@pytest.mark.parametrize("path", ("/data/posts.json", "/data/posts/1.json"))
async def test_url_column_type_full_link_text_in_json(ds_long_url, path):
# truncate_cells_html only applies to the HTML table view
await ds_long_url.invoke_startup()
response = await ds_long_url.client.get(path + "?_extra=render_cell")
assert response.status_code == 200
rendered = response.json()["render_cell"][0]["website"]
assert rendered == f'<a href="{LONG_URL}">{LONG_URL}</a>'
@pytest.mark.asyncio
async def test_url_column_type_non_http_value_unchanged(ds_long_url):
await ds_long_url.invoke_startup()
response = await ds_long_url.client.get("/data/posts")
assert response.status_code == 200
td = Soup(response.text, "html.parser").select("td.col-website")[1]
assert td.find("a") is None
assert td.text == "not a url"
@pytest.mark.asyncio
async def test_set_column_type_ui_data_hidden_without_permission(ds_ct):
await ds_ct.invoke_startup()