RowContext - row page now renders a documented Context dataclass

RowView declares context_class = RowContext; BaseView.render()
constructs the dataclass from the assembled context, dropping any keys
not declared on the class, after select_templates and
alternate_url_json have been added. Extras-named fields use
extra_field() so their documentation comes from the Extra classes;
view-added fields carry help metadata next to the view code.

Refs #2127

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-06-11 07:01:57 -07:00
commit 3cc0fc07b4
3 changed files with 97 additions and 1 deletions

View file

@ -1,5 +1,6 @@
import asyncio
import csv
import dataclasses
import hashlib
import sys
import textwrap
@ -88,6 +89,9 @@ class View:
class BaseView:
ds = None
has_json_alternate = True
# Set to a Context subclass to render a documented template context -
# keys not declared on the class are dropped before rendering
context_class = None
def __init__(self, datasette):
self.ds = datasette
@ -169,6 +173,11 @@ class BaseView:
)
}
)
if self.context_class is not None:
declared = {f.name for f in dataclasses.fields(self.context_class)}
template_context = self.context_class(
**{k: v for k, v in template_context.items() if k in declared}
)
return Response.html(
await self.ds.render_template(
template,

View file

@ -11,16 +11,95 @@ from datasette.utils import (
escape_sqlite,
)
from datasette.plugins import pm
from dataclasses import dataclass, field
import json
import markupsafe
import sqlite_utils
from datasette.extras import extra_names_from_request
from datasette.extras import extra_names_from_request, ExtraScope
from . import Context, extra_field
from .table import display_columns_and_rows
from .table_extras import RowExtraContext, resolve_row_extras, table_extra_registry
@dataclass
class RowContext(Context):
"The page showing an individual row, e.g. /fixtures/facetable/1"
extras_scope = ExtraScope.ROW
# Fields resolved by registered extras - their documentation comes
# from the description on each Extra class in table_extras.py
columns: list = extra_field()
database: str = extra_field()
database_color: str = extra_field()
foreign_key_tables: list = extra_field()
metadata: dict = extra_field()
primary_keys: list = extra_field()
private: bool = extra_field()
table: str = extra_field()
# Fields added by the view code
ok: bool = field(
metadata={"help": "True if the data for this page was retrieved without errors"}
)
rows: list = field(
metadata={
"help": "The rows for this page, as a list of dictionaries mapping column name to value"
}
)
primary_key_values: list = field(
metadata={"help": "Values of the primary keys for this row, from the URL"}
)
query_ms: float = field(
metadata={
"help": "Time taken by the SQL queries for this page, in milliseconds"
}
)
display_columns: list = field(
metadata={"help": "Column objects formatted for the HTML table display"}
)
display_rows: list = field(
metadata={"help": "Row data formatted for the HTML table display"}
)
custom_table_templates: list = field(
metadata={
"help": "Custom template names that were considered for displaying this table"
}
)
row_actions: list = field(
metadata={"help": "Row actions made available by plugin hooks"}
)
top_row: callable = field(
metadata={"help": "Async function rendering the top_row plugin slot"}
)
renderers: dict = field(
metadata={
"help": "Dictionary mapping output format names (e.g. json) to their URLs for this page"
}
)
url_csv: str = field(metadata={"help": "URL for the CSV export of this page"})
url_csv_path: str = field(metadata={"help": "Path portion of the CSV export URL"})
url_csv_hidden_args: list = field(
metadata={
"help": "(name, value) pairs for hidden form fields used by the CSV export form"
}
)
settings: dict = field(
metadata={"help": "Dictionary of Datasette's current settings"}
)
select_templates: list = field(
metadata={
"help": "List of template names that were considered for this page, the one used marked with an asterisk"
}
)
alternate_url_json: str = field(
metadata={"help": "URL for the JSON version of this page"}
)
class RowView(DataView):
name = "row"
context_class = RowContext
async def data(self, request, default_labels=False):
resolved = await self.ds.resolve_row(request)

View file

@ -186,6 +186,14 @@ def test_table_context_fields_match_documented_contract():
}
def test_row_context_fields_match_documented_contract():
from datasette.views.row import RowContext
assert {f.name for f in RowContext.documented_fields()} == {
key.name for key in PAGES["row"].documented_keys()
}
def test_base_context_keys_all_have_docs():
for key in BASE_CONTEXT_KEYS:
assert key.doc, "Base context key {} is missing docs".format(key.name)