diff --git a/datasette/views/database.py b/datasette/views/database.py index bdd433cc..9a8aca32 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -11,6 +11,7 @@ from datasette.utils import ( add_cors_headers, await_me_maybe, derive_named_parameters, + format_bytes, tilde_decode, to_css_class, validate_sql_select, @@ -399,13 +400,18 @@ class QueryView(DataView): ).hexdigest(), }, ) - display_value = Markup( - '<Binary: {} byte{}>'.format( + formatted = format_bytes(len(value)) + display_value = markupsafe.Markup( + '<Binary: {:,} byte{}>'.format( blob_url, - len(display_value), + ' title="{}"'.format(formatted) + if "bytes" not in formatted + else "", + len(value), "" if len(value) == 1 else "s", ) ) + display_row.append(display_value) display_rows.append(display_row) diff --git a/datasette/views/table.py b/datasette/views/table.py index cd7afea6..dc85165e 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -12,10 +12,12 @@ from datasette.utils import ( MultiParams, append_querystring, compound_keys_after_sql, + format_bytes, tilde_decode, tilde_encode, escape_sqlite, filters_should_redirect, + format_bytes, is_url, path_from_row_pks, path_with_added_args, @@ -175,14 +177,18 @@ class RowTableShared(DataView): if plugin_display_value: display_value = plugin_display_value elif isinstance(value, bytes): + formatted = format_bytes(len(value)) display_value = markupsafe.Markup( - '<Binary: {} byte{}>'.format( + '<Binary: {:,} byte{}>'.format( self.ds.urls.row_blob( database, table, path_from_row_pks(row, pks, not pks), column, ), + ' title="{}"'.format(formatted) + if "bytes" not in formatted + else "", len(value), "" if len(value) == 1 else "s", ) diff --git a/tests/test_table_html.py b/tests/test_table_html.py index 6dc26434..d3cb3e17 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -1,3 +1,4 @@ +from datasette.app import Datasette, Database from bs4 import BeautifulSoup as Soup from .fixtures import ( # noqa app_client, @@ -1089,3 +1090,28 @@ def test_allow_facet_off(allow_facet): assert "Suggested facets" in response.text else: assert "Suggested facets" not in response.text + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "size,title,length_bytes", + ( + (2000, ' title="2.0 KB"', "2,000"), + (20000, ' title="19.5 KB"', "20,000"), + (20, "", "20"), + ), +) +async def test_format_of_binary_links(size, title, length_bytes): + ds = Datasette() + db_name = "binary-links-{}".format(size) + db = ds.add_memory_database(db_name) + sql = "select zeroblob({}) as blob".format(size) + await db.execute_write("create table blobs as {}".format(sql)) + response = await ds.client.get("/{}/blobs".format(db_name)) + assert response.status_code == 200 + expected = "{}><Binary: {} bytes>".format(title, length_bytes) + assert expected in response.text + # And test with arbitrary SQL query too + sql_response = await ds.client.get("/{}".format(db_name), params={"sql": sql}) + assert sql_response.status_code == 200 + assert expected in sql_response.text