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"] == [