diff --git a/datasette/views/row.py b/datasette/views/row.py
index 4f896632..077c33c2 100644
--- a/datasette/views/row.py
+++ b/datasette/views/row.py
@@ -12,7 +12,7 @@ from datasette.utils import (
from datasette.plugins import pm
import json
import sqlite_utils
-from .table import display_columns_and_rows
+from .table import display_columns_and_rows, _get_extras
class RowView(DataView):
@@ -104,11 +104,48 @@ class RowView(DataView):
"primary_key_values": pk_values,
}
+ # Handle _extra parameter (new style)
+ extras = _get_extras(request)
+
+ # Also support legacy _extras parameter for backward compatibility
if "foreign_key_tables" in (request.args.get("_extras") or "").split(","):
+ extras.add("foreign_key_tables")
+
+ # Process extras
+ if "foreign_key_tables" in extras:
data["foreign_key_tables"] = await self.foreign_key_tables(
database, table, pk_values
)
+ if "render_cell" in extras:
+ # Call render_cell plugin hook for each cell
+ rendered_rows = []
+ for row in rows:
+ rendered_row = {}
+ for value, column in zip(row, columns):
+ # Call render_cell plugin hook
+ plugin_display_value = None
+ for candidate in pm.hook.render_cell(
+ row=row,
+ value=value,
+ column=column,
+ table=table,
+ database=database,
+ datasette=self.ds,
+ request=request,
+ ):
+ candidate = await await_me_maybe(candidate)
+ if candidate is not None:
+ plugin_display_value = candidate
+ break
+ if plugin_display_value:
+ rendered_row[column] = str(plugin_display_value)
+ else:
+ # Default: convert value to string
+ rendered_row[column] = "" if value is None else str(value)
+ rendered_rows.append(rendered_row)
+ data["render_cell"] = rendered_rows
+
return (
data,
template_data,
diff --git a/datasette/views/table.py b/datasette/views/table.py
index c8f209d6..9a3ae69f 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -1492,7 +1492,7 @@ async def table_view_data(
async def extra_display_rows(run_display_columns_and_rows):
return run_display_columns_and_rows["rows"]
- async def extra_render_cells():
+ async def extra_render_cell():
"Rendered HTML for each cell using the render_cell plugin hook"
columns = [col[0] for col in results.description]
rendered_rows = []
@@ -1708,7 +1708,7 @@ async def table_view_data(
run_display_columns_and_rows,
extra_display_columns,
extra_display_rows,
- extra_render_cells,
+ extra_render_cell,
extra_debug,
extra_request,
extra_query,
diff --git a/tests/test_api.py b/tests/test_api.py
index 008fc42b..1571fd5d 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -752,6 +752,62 @@ async def test_row_foreign_key_tables(ds_client):
]
+@pytest.mark.asyncio
+async def test_row_extra_render_cell():
+ """Test that _extra=render_cell returns rendered HTML from render_cell plugin hook on row pages"""
+ from datasette import hookimpl
+ from datasette.app import Datasette
+
+ class TestRenderCellPlugin:
+ __name__ = "TestRenderCellPlugin"
+
+ @hookimpl
+ def render_cell(self, value, column, table, database):
+ # Only modify cells in our test table
+ if table == "test_render" and column == "name":
+ return f"{value}"
+ return None
+
+ ds = Datasette(memory=True)
+ await ds.invoke_startup()
+ db = ds.add_memory_database("test_row_render")
+ await db.execute_write(
+ "create table test_render (id integer primary key, name text)"
+ )
+ await db.execute_write("insert into test_render values (1, 'Alice')")
+
+ # Register our test plugin
+ ds.pm.register(TestRenderCellPlugin(), name="TestRenderCellPlugin")
+
+ try:
+ # Request row with _extra=render_cell
+ response = await ds.client.get(
+ "/test_row_render/test_render/1.json?_extra=render_cell"
+ )
+ assert response.status_code == 200
+ data = response.json()
+
+ # Verify the response structure
+ assert "render_cell" in data
+ assert "rows" in data
+
+ # render_cell should be a list with one row (since this is a row page)
+ render_cell = data["render_cell"]
+ assert len(render_cell) == 1
+
+ # The row: id=1, name='Alice'
+ # The 'name' column should be rendered by our plugin as Alice
+ assert render_cell[0]["name"] == "Alice"
+ # The 'id' column should use default rendering (just the value as string)
+ assert render_cell[0]["id"] == "1"
+
+ # The regular rows should still contain raw values
+ assert data["rows"] == [{"id": 1, "name": "Alice"}]
+
+ finally:
+ ds.pm.unregister(name="TestRenderCellPlugin")
+
+
def test_databases_json(app_client_two_attached_databases_one_immutable):
response = app_client_two_attached_databases_one_immutable.get("/-/databases.json")
databases = response.json
diff --git a/tests/test_table_api.py b/tests/test_table_api.py
index d5a8ca41..25419bb8 100644
--- a/tests/test_table_api.py
+++ b/tests/test_table_api.py
@@ -1386,8 +1386,8 @@ async def test_table_extras(ds_client, extra, expected_json):
@pytest.mark.asyncio
-async def test_extra_render_cells():
- """Test that _extra=render_cells returns rendered HTML from render_cell plugin hook"""
+async def test_extra_render_cell():
+ """Test that _extra=render_cell returns rendered HTML from render_cell plugin hook"""
from datasette import hookimpl
from datasette.app import Datasette
@@ -1403,7 +1403,7 @@ async def test_extra_render_cells():
ds = Datasette(memory=True)
await ds.invoke_startup()
- db = ds.add_memory_database("test")
+ db = ds.add_memory_database("test_table_render")
await db.execute_write(
"create table test_render (id integer primary key, name text)"
)
@@ -1414,28 +1414,30 @@ async def test_extra_render_cells():
ds.pm.register(TestRenderCellPlugin(), name="TestRenderCellPlugin")
try:
- # Request with _extra=render_cells
- response = await ds.client.get("/test/test_render.json?_extra=render_cells")
+ # Request with _extra=render_cell
+ response = await ds.client.get(
+ "/test_table_render/test_render.json?_extra=render_cell"
+ )
assert response.status_code == 200
data = response.json()
# Verify the response structure
- assert "render_cells" in data
+ assert "render_cell" in data
assert "rows" in data
- # render_cells should be a list of rows, each row being a dict of column -> rendered HTML
- render_cells = data["render_cells"]
- assert len(render_cells) == 2
+ # render_cell should be a list of rows, each row being a dict of column -> rendered HTML
+ render_cell = data["render_cell"]
+ assert len(render_cell) == 2
# First row: id=1, name='Alice'
# The 'name' column should be rendered by our plugin as Alice
- assert render_cells[0]["name"] == "Alice"
+ assert render_cell[0]["name"] == "Alice"
# The 'id' column should use default rendering (just the value as string)
- assert render_cells[0]["id"] == "1"
+ assert render_cell[0]["id"] == "1"
# Second row: id=2, name='Bob'
- assert render_cells[1]["name"] == "Bob"
- assert render_cells[1]["id"] == "2"
+ assert render_cell[1]["name"] == "Bob"
+ assert render_cell[1]["id"] == "2"
# The regular rows should still contain raw values
assert data["rows"] == [