mirror of
https://github.com/simonw/datasette.git
synced 2026-06-28 11:44:34 +02:00
One consistent pattern: every page context is a Context dataclass
datasette/template_contexts.py is now a thin index with no documentation strings of its own - the docs live next to the code: - Each page's Context class (DatabaseContext, QueryContext, TableContext, RowContext) carries a docstring, its template name and help metadata on view-added fields, in the view module itself - extra_field() fields document themselves from the Extra classes - The keys render_template() adds to every page are documented in TEMPLATE_BASE_CONTEXT in app.py, next to the code that adds them, with the contract tests keeping the two in sync docs/template_context.rst is regenerated from the dataclasses, so the table and row pages now include field types like the others. Refs #2127 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3cc0fc07b4
commit
8e01542fe9
8 changed files with 184 additions and 387 deletions
|
|
@ -313,6 +313,32 @@ def _to_string(value):
|
|||
return json.dumps(value, default=str)
|
||||
|
||||
|
||||
# Documentation for the variables Datasette.render_template() adds to the
|
||||
# context for every page. This is part of the documented template contract:
|
||||
# keys added in render_template() must be documented here - the contract
|
||||
# tests in tests/test_template_context.py enforce this, and the docs in
|
||||
# docs/template_context.rst are generated from it.
|
||||
TEMPLATE_BASE_CONTEXT = {
|
||||
"request": "The current Request object, or None",
|
||||
"crumb_items": "Async function returning breadcrumb navigation items for the current page",
|
||||
"urls": "Object with methods for constructing URLs to pages within Datasette - see datasette.urls in the internals documentation",
|
||||
"actor": "The currently authenticated actor dictionary, or None",
|
||||
"menu_links": "Async function returning links for the Datasette application menu, including those added by plugins",
|
||||
"display_actor": "Function returning a display string for an actor dictionary",
|
||||
"show_logout": "True if the logout link should be shown in the navigation menu",
|
||||
"app_css_hash": "Hash of Datasette's app.css contents, used for cache busting",
|
||||
"zip": "Python's zip() builtin, made available to template logic",
|
||||
"body_scripts": "List of script blocks for the page body contributed by plugins",
|
||||
"format_bytes": "Function that formats a number of bytes as a human-readable size",
|
||||
"show_messages": "Function returning any messages set for the current user, clearing them in the process",
|
||||
"extra_css_urls": "List of {url, sri} dictionaries of extra CSS stylesheets to include on the page, from plugins and configuration",
|
||||
"extra_js_urls": "List of {url, sri, module} dictionaries of extra JavaScript URLs to include on the page",
|
||||
"base_url": "The configured base_url setting",
|
||||
"csrftoken": "Function returning the CSRF token for the current request",
|
||||
"datasette_version": "The version of Datasette that is running",
|
||||
}
|
||||
|
||||
|
||||
class Datasette:
|
||||
# Message constants:
|
||||
INFO = 1
|
||||
|
|
@ -2216,6 +2242,8 @@ class Datasette:
|
|||
links.extend(extra_links)
|
||||
return links
|
||||
|
||||
# Keys added here must be documented in TEMPLATE_BASE_CONTEXT -
|
||||
# the contract tests fail otherwise
|
||||
template_context = {
|
||||
**context,
|
||||
**{
|
||||
|
|
|
|||
|
|
@ -1,240 +1,40 @@
|
|||
"""
|
||||
The documented template context for Datasette's core HTML pages.
|
||||
Index of the documented template contexts for Datasette's core HTML pages.
|
||||
|
||||
This module is the source of truth for the template context contract:
|
||||
the set of variables that custom templates can rely on for each page.
|
||||
It is consumed by the contract tests in tests/test_template_context.py,
|
||||
which assert that the real rendered context for each page exactly
|
||||
matches what is documented here, and by docs/template_context_doc.py
|
||||
which generates the documentation in docs/template_context.rst.
|
||||
This module deliberately contains no documentation strings of its own -
|
||||
the documentation lives next to the code it describes:
|
||||
|
||||
Documentation for each key comes from one of three places:
|
||||
- Every page renders a Context dataclass defined in its view module
|
||||
(DatabaseContext, QueryContext in views/database.py, TableContext in
|
||||
views/table.py, RowContext in views/row.py). Fields added by view code
|
||||
carry ``help`` metadata; fields declared with extra_field() take their
|
||||
documentation from the description on the matching Extra class in
|
||||
views/table_extras.py.
|
||||
- The keys render_template() adds to every page are documented in
|
||||
TEMPLATE_BASE_CONTEXT in datasette/app.py, next to the code that adds
|
||||
them.
|
||||
|
||||
- Pages that render a Context dataclass (database, query) use the
|
||||
``help`` metadata on each dataclass field
|
||||
- Keys provided by registered extras use the ``description`` from the
|
||||
Extra class
|
||||
- Keys added inline by view code are documented in this module
|
||||
The contract tests in tests/test_template_context.py assert that the real
|
||||
rendered context for each page exactly matches what is documented, and
|
||||
docs/template_context_doc.py generates docs/template_context.rst from the
|
||||
same classes.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from datasette.extras import ExtraScope
|
||||
from datasette.app import TEMPLATE_BASE_CONTEXT
|
||||
from datasette.views.database import DatabaseContext, QueryContext
|
||||
from datasette.views.table_extras import table_extra_registry
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TemplateContextKey:
|
||||
name: str
|
||||
doc: str
|
||||
|
||||
|
||||
def _keys(**docs):
|
||||
return tuple(TemplateContextKey(name, doc) for name, doc in docs.items())
|
||||
|
||||
|
||||
# Added by Datasette.render_template() to the context for every page,
|
||||
# including pages rendered by plugins
|
||||
BASE_CONTEXT_KEYS = _keys(
|
||||
request="The current Request object, or None",
|
||||
crumb_items="Async function returning breadcrumb navigation items for the current page",
|
||||
urls="Object with methods for constructing URLs to pages within Datasette - see datasette.urls in the internals documentation",
|
||||
actor="The currently authenticated actor dictionary, or None",
|
||||
menu_links="Async function returning links for the Datasette application menu, including those added by plugins",
|
||||
display_actor="Function returning a display string for an actor dictionary",
|
||||
show_logout="True if the logout link should be shown in the navigation menu",
|
||||
app_css_hash="Hash of Datasette's app.css contents, used for cache busting",
|
||||
zip="Python's zip() builtin, made available to template logic",
|
||||
body_scripts="List of script blocks for the page body contributed by plugins",
|
||||
format_bytes="Function that formats a number of bytes as a human-readable size",
|
||||
show_messages="Function returning any messages set for the current user, clearing them in the process",
|
||||
extra_css_urls="List of {url, sri} dictionaries of extra CSS stylesheets to include on the page, from plugins and configuration",
|
||||
extra_js_urls="List of {url, sri, module} dictionaries of extra JavaScript URLs to include on the page",
|
||||
base_url="The configured base_url setting",
|
||||
csrftoken="Function returning the CSRF token for the current request",
|
||||
datasette_version="The version of Datasette that is running",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageContext:
|
||||
# Identifier used in tests and documentation, e.g. "table"
|
||||
name: str
|
||||
title: str
|
||||
description: str
|
||||
# The default template used to render this page
|
||||
template: str
|
||||
# For pages rendered from a Context dataclass
|
||||
context_class: type = None
|
||||
# For pages whose context includes resolved extras
|
||||
extras_scope: ExtraScope = None
|
||||
extra_keys: tuple = ()
|
||||
# Keys added inline by the view code, documented here
|
||||
keys: tuple = ()
|
||||
|
||||
def documented_keys(self):
|
||||
"Every page-specific documented key, excluding BASE_CONTEXT_KEYS"
|
||||
documented = []
|
||||
if self.context_class is not None:
|
||||
documented.extend(
|
||||
TemplateContextKey(f.name, f.help)
|
||||
for f in self.context_class.documented_fields()
|
||||
)
|
||||
for name in self.extra_keys:
|
||||
cls = table_extra_registry.classes_by_name[name]
|
||||
documented.append(TemplateContextKey(name, cls.description or ""))
|
||||
documented.extend(self.keys)
|
||||
return sorted(documented, key=lambda key: key.name)
|
||||
|
||||
|
||||
_SHARED_DOCS = dict(
|
||||
ok="True if the data for this page was retrieved without errors",
|
||||
rows="The rows for this page, as a list of dictionaries mapping column name to value",
|
||||
query_ms="Time taken by the SQL queries for this page, in milliseconds",
|
||||
select_templates="List of template names that were considered for this page, the one used marked with an asterisk",
|
||||
settings="Dictionary of Datasette's current settings",
|
||||
alternate_url_json="URL for the JSON version of this page",
|
||||
url_csv="URL for the CSV export of this page",
|
||||
url_csv_path="Path portion of the CSV export URL",
|
||||
url_csv_hidden_args="(name, value) pairs for hidden form fields used by the CSV export form",
|
||||
renderers="Dictionary mapping output format names (e.g. json) to their URLs for this page",
|
||||
display_columns="Column objects formatted for the HTML table display",
|
||||
display_rows="Row data formatted for the HTML table display",
|
||||
custom_table_templates="Custom template names that were considered for displaying this table",
|
||||
)
|
||||
|
||||
|
||||
def _shared(*names):
|
||||
return tuple(TemplateContextKey(name, _SHARED_DOCS[name]) for name in names)
|
||||
|
||||
from datasette.views.row import RowContext
|
||||
from datasette.views.table import TableContext
|
||||
|
||||
PAGES = {
|
||||
page.name: page
|
||||
for page in (
|
||||
PageContext(
|
||||
name="database",
|
||||
title="Database",
|
||||
description="The page listing the tables, views and queries in a database, e.g. /fixtures.",
|
||||
template="database.html",
|
||||
context_class=DatabaseContext,
|
||||
),
|
||||
PageContext(
|
||||
name="query",
|
||||
title="Query",
|
||||
description="The page for arbitrary SQL queries (/database/-/query?sql=...) and stored queries (/database/query-name).",
|
||||
template="query.html",
|
||||
context_class=QueryContext,
|
||||
),
|
||||
PageContext(
|
||||
name="table",
|
||||
title="Table",
|
||||
description="The page showing the rows in a table or SQL view, e.g. /fixtures/facetable.",
|
||||
template="table.html",
|
||||
extras_scope=ExtraScope.TABLE,
|
||||
extra_keys=(
|
||||
"actions",
|
||||
"all_columns",
|
||||
"columns",
|
||||
"count",
|
||||
"count_sql",
|
||||
"custom_table_templates",
|
||||
"database",
|
||||
"database_color",
|
||||
"display_columns",
|
||||
"display_rows",
|
||||
"expandable_columns",
|
||||
"facet_results",
|
||||
"facets_timed_out",
|
||||
"filters",
|
||||
"form_hidden_args",
|
||||
"human_description_en",
|
||||
"is_view",
|
||||
"metadata",
|
||||
"next_url",
|
||||
"primary_keys",
|
||||
"private",
|
||||
"query",
|
||||
"renderers",
|
||||
"set_column_type_ui",
|
||||
"sorted_facet_results",
|
||||
"suggested_facets",
|
||||
"table",
|
||||
"table_definition",
|
||||
"view_definition",
|
||||
),
|
||||
keys=_shared(
|
||||
"ok",
|
||||
"rows",
|
||||
"query_ms",
|
||||
"select_templates",
|
||||
"settings",
|
||||
"alternate_url_json",
|
||||
"url_csv",
|
||||
"url_csv_path",
|
||||
"url_csv_hidden_args",
|
||||
)
|
||||
+ _keys(
|
||||
allow_execute_sql="True if the current actor can execute custom SQL against this database",
|
||||
append_querystring="Function that appends additional querystring arguments to a URL",
|
||||
count_limit="The maximum number of rows Datasette will count before showing an approximation",
|
||||
datasette_allow_facet='The string "true" or "false" reflecting the allow_facet setting',
|
||||
extra_wheres_for_ui="Extra where clauses from ?_where=, with links to remove them",
|
||||
filter_columns="List of columns offered by the filter interface",
|
||||
fix_path="Function that applies the base_url prefix to a path",
|
||||
is_sortable="True if any of the displayed columns can be used to sort",
|
||||
next="Pagination token for the next page, or None",
|
||||
path_with_replaced_args="Function for building the current path with modified querystring arguments",
|
||||
sort="Column the page is sorted by, or None",
|
||||
sort_desc="Column the page is sorted by in descending order, or None",
|
||||
supports_search="True if this table has full-text search configured",
|
||||
top_table="Async function rendering the top_table plugin slot",
|
||||
),
|
||||
),
|
||||
PageContext(
|
||||
name="row",
|
||||
title="Row",
|
||||
description="The page showing an individual row, e.g. /fixtures/facetable/1.",
|
||||
template="row.html",
|
||||
extras_scope=ExtraScope.ROW,
|
||||
extra_keys=(
|
||||
"columns",
|
||||
"database",
|
||||
"database_color",
|
||||
"foreign_key_tables",
|
||||
"metadata",
|
||||
"primary_keys",
|
||||
"private",
|
||||
"table",
|
||||
),
|
||||
keys=_shared(
|
||||
"ok",
|
||||
"rows",
|
||||
"query_ms",
|
||||
"select_templates",
|
||||
"settings",
|
||||
"alternate_url_json",
|
||||
"url_csv",
|
||||
"url_csv_path",
|
||||
"url_csv_hidden_args",
|
||||
"renderers",
|
||||
"display_columns",
|
||||
"display_rows",
|
||||
"custom_table_templates",
|
||||
)
|
||||
+ _keys(
|
||||
primary_key_values="Values of the primary keys for this row, from the URL",
|
||||
row_actions="Row actions made available by plugin hooks",
|
||||
top_row="Async function rendering the top_row plugin slot",
|
||||
),
|
||||
),
|
||||
)
|
||||
"database": DatabaseContext,
|
||||
"query": QueryContext,
|
||||
"table": TableContext,
|
||||
"row": RowContext,
|
||||
}
|
||||
|
||||
|
||||
def documented_context_keys(page_name):
|
||||
"Set of every documented key for the named page, including base context keys"
|
||||
page = PAGES[page_name]
|
||||
return {key.name for key in BASE_CONTEXT_KEYS} | {
|
||||
key.name for key in page.documented_keys()
|
||||
return set(TEMPLATE_BASE_CONTEXT) | {
|
||||
f.name for f in PAGES[page_name].documented_fields()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,6 +228,10 @@ class DatabaseView(View):
|
|||
|
||||
@dataclass
|
||||
class DatabaseContext(Context):
|
||||
"The page listing the tables, views and queries in a database, e.g. /fixtures."
|
||||
|
||||
template = "database.html"
|
||||
|
||||
database: str = field(metadata={"help": "The name of the database"})
|
||||
private: bool = field(
|
||||
metadata={"help": "Boolean indicating if this is a private database"}
|
||||
|
|
@ -281,6 +285,10 @@ class DatabaseContext(Context):
|
|||
|
||||
@dataclass
|
||||
class QueryContext(Context):
|
||||
"The page for arbitrary SQL queries (/database/-/query?sql=...) and stored queries (/database/query-name)."
|
||||
|
||||
template = "query.html"
|
||||
|
||||
database: str = field(metadata={"help": "The name of the database being queried"})
|
||||
database_color: str = field(metadata={"help": "The color of the database"})
|
||||
query: dict = field(
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ from .table_extras import RowExtraContext, resolve_row_extras, table_extra_regis
|
|||
|
||||
@dataclass
|
||||
class RowContext(Context):
|
||||
"The page showing an individual row, e.g. /fixtures/facetable/1"
|
||||
"The page showing an individual row, e.g. /fixtures/facetable/1."
|
||||
|
||||
template = "row.html"
|
||||
extras_scope = ExtraScope.ROW
|
||||
|
||||
# Fields resolved by registered extras - their documentation comes
|
||||
|
|
|
|||
|
|
@ -59,8 +59,9 @@ from .table_extras import (
|
|||
|
||||
@dataclass
|
||||
class TableContext(Context):
|
||||
"The page showing the rows in a table or SQL view, e.g. /fixtures/facetable"
|
||||
"The page showing the rows in a table or SQL view, e.g. /fixtures/facetable."
|
||||
|
||||
template = "table.html"
|
||||
extras_scope = ExtraScope.TABLE
|
||||
|
||||
# Fields resolved by registered extras - their documentation comes
|
||||
|
|
|
|||
|
|
@ -250,160 +250,160 @@ The page showing the rows in a table or SQL view, e.g. /fixtures/facetable. Rend
|
|||
|
||||
Many of these keys are shared with the :ref:`JSON API <json_api>` for this page.
|
||||
|
||||
``actions``
|
||||
``actions`` - ``callable``
|
||||
Table or view actions made available by plugin hooks
|
||||
|
||||
``all_columns``
|
||||
``all_columns`` - ``list``
|
||||
All columns in the table, regardless of _col/_nocol filtering
|
||||
|
||||
``allow_execute_sql``
|
||||
``allow_execute_sql`` - ``bool``
|
||||
True if the current actor can execute custom SQL against this database
|
||||
|
||||
``alternate_url_json``
|
||||
``alternate_url_json`` - ``str``
|
||||
URL for the JSON version of this page
|
||||
|
||||
``append_querystring``
|
||||
``append_querystring`` - ``callable``
|
||||
Function that appends additional querystring arguments to a URL
|
||||
|
||||
``columns``
|
||||
``columns`` - ``list``
|
||||
Column names returned by this query
|
||||
|
||||
``count``
|
||||
``count`` - ``int``
|
||||
Total count of rows matching these filters
|
||||
|
||||
``count_limit``
|
||||
``count_limit`` - ``int``
|
||||
The maximum number of rows Datasette will count before showing an approximation
|
||||
|
||||
``count_sql``
|
||||
``count_sql`` - ``str``
|
||||
SQL query used to calculate the total count
|
||||
|
||||
``custom_table_templates``
|
||||
``custom_table_templates`` - ``list``
|
||||
Custom template names considered for this table
|
||||
|
||||
``database``
|
||||
``database`` - ``str``
|
||||
Database name
|
||||
|
||||
``database_color``
|
||||
``database_color`` - ``str``
|
||||
Color assigned to the database
|
||||
|
||||
``datasette_allow_facet``
|
||||
``datasette_allow_facet`` - ``str``
|
||||
The string "true" or "false" reflecting the allow_facet setting
|
||||
|
||||
``display_columns``
|
||||
``display_columns`` - ``list``
|
||||
Column metadata used by the HTML table display
|
||||
|
||||
``display_rows``
|
||||
``display_rows`` - ``list``
|
||||
Row data formatted for the HTML table display
|
||||
|
||||
``expandable_columns``
|
||||
``expandable_columns`` - ``list``
|
||||
Foreign key columns that can be expanded with labels
|
||||
|
||||
``extra_wheres_for_ui``
|
||||
``extra_wheres_for_ui`` - ``list``
|
||||
Extra where clauses from ?_where=, with links to remove them
|
||||
|
||||
``facet_results``
|
||||
``facet_results`` - ``dict``
|
||||
Results of facets calculated against this data
|
||||
|
||||
``facets_timed_out``
|
||||
``facets_timed_out`` - ``list``
|
||||
Facet calculations that timed out
|
||||
|
||||
``filter_columns``
|
||||
``filter_columns`` - ``list``
|
||||
List of columns offered by the filter interface
|
||||
|
||||
``filters``
|
||||
``filters`` - ``Filters``
|
||||
Filters object used by the HTML table interface
|
||||
|
||||
``fix_path``
|
||||
``fix_path`` - ``callable``
|
||||
Function that applies the base_url prefix to a path
|
||||
|
||||
``form_hidden_args``
|
||||
``form_hidden_args`` - ``list``
|
||||
Hidden form arguments used by the HTML table interface
|
||||
|
||||
``human_description_en``
|
||||
``human_description_en`` - ``str``
|
||||
Human-readable description of the filters
|
||||
|
||||
``is_sortable``
|
||||
``is_sortable`` - ``bool``
|
||||
True if any of the displayed columns can be used to sort
|
||||
|
||||
``is_view``
|
||||
``is_view`` - ``bool``
|
||||
Whether this resource is a view instead of a table
|
||||
|
||||
``metadata``
|
||||
``metadata`` - ``dict``
|
||||
Metadata about the table, database or stored query
|
||||
|
||||
``next``
|
||||
``next`` - ``str``
|
||||
Pagination token for the next page, or None
|
||||
|
||||
``next_url``
|
||||
``next_url`` - ``str``
|
||||
Full URL for the next page of results
|
||||
|
||||
``ok``
|
||||
``ok`` - ``bool``
|
||||
True if the data for this page was retrieved without errors
|
||||
|
||||
``path_with_replaced_args``
|
||||
``path_with_replaced_args`` - ``callable``
|
||||
Function for building the current path with modified querystring arguments
|
||||
|
||||
``primary_keys``
|
||||
``primary_keys`` - ``list``
|
||||
Primary keys for this table
|
||||
|
||||
``private``
|
||||
``private`` - ``bool``
|
||||
Whether this resource is private to the current actor
|
||||
|
||||
``query``
|
||||
``query`` - ``dict``
|
||||
Details of the underlying SQL query
|
||||
|
||||
``query_ms``
|
||||
``query_ms`` - ``float``
|
||||
Time taken by the SQL queries for this page, in milliseconds
|
||||
|
||||
``renderers``
|
||||
``renderers`` - ``dict``
|
||||
Alternative output renderers available for this table
|
||||
|
||||
``rows``
|
||||
``rows`` - ``list``
|
||||
The rows for this page, as a list of dictionaries mapping column name to value
|
||||
|
||||
``select_templates``
|
||||
``select_templates`` - ``list``
|
||||
List of template names that were considered for this page, the one used marked with an asterisk
|
||||
|
||||
``set_column_type_ui``
|
||||
``set_column_type_ui`` - ``dict``
|
||||
Column type UI metadata for this table
|
||||
|
||||
``settings``
|
||||
``settings`` - ``dict``
|
||||
Dictionary of Datasette's current settings
|
||||
|
||||
``sort``
|
||||
``sort`` - ``str``
|
||||
Column the page is sorted by, or None
|
||||
|
||||
``sort_desc``
|
||||
``sort_desc`` - ``str``
|
||||
Column the page is sorted by in descending order, or None
|
||||
|
||||
``sorted_facet_results``
|
||||
``sorted_facet_results`` - ``list``
|
||||
Facet results sorted for display
|
||||
|
||||
``suggested_facets``
|
||||
``suggested_facets`` - ``list``
|
||||
Suggestions for facets that might return interesting results
|
||||
|
||||
``supports_search``
|
||||
``supports_search`` - ``bool``
|
||||
True if this table has full-text search configured
|
||||
|
||||
``table``
|
||||
``table`` - ``str``
|
||||
Table name
|
||||
|
||||
``table_definition``
|
||||
``table_definition`` - ``str``
|
||||
SQL definition for this table
|
||||
|
||||
``top_table``
|
||||
``top_table`` - ``callable``
|
||||
Async function rendering the top_table plugin slot
|
||||
|
||||
``url_csv``
|
||||
``url_csv`` - ``str``
|
||||
URL for the CSV export of this page
|
||||
|
||||
``url_csv_hidden_args``
|
||||
``url_csv_hidden_args`` - ``list``
|
||||
(name, value) pairs for hidden form fields used by the CSV export form
|
||||
|
||||
``url_csv_path``
|
||||
``url_csv_path`` - ``str``
|
||||
Path portion of the CSV export URL
|
||||
|
||||
``view_definition``
|
||||
``view_definition`` - ``str``
|
||||
SQL definition for this view
|
||||
|
||||
Row page
|
||||
|
|
@ -413,76 +413,76 @@ The page showing an individual row, e.g. /fixtures/facetable/1. Rendered using t
|
|||
|
||||
Many of these keys are shared with the :ref:`JSON API <json_api>` for this page.
|
||||
|
||||
``alternate_url_json``
|
||||
``alternate_url_json`` - ``str``
|
||||
URL for the JSON version of this page
|
||||
|
||||
``columns``
|
||||
``columns`` - ``list``
|
||||
Column names returned by this query
|
||||
|
||||
``custom_table_templates``
|
||||
``custom_table_templates`` - ``list``
|
||||
Custom template names that were considered for displaying this table
|
||||
|
||||
``database``
|
||||
``database`` - ``str``
|
||||
Database name
|
||||
|
||||
``database_color``
|
||||
``database_color`` - ``str``
|
||||
Color assigned to the database
|
||||
|
||||
``display_columns``
|
||||
``display_columns`` - ``list``
|
||||
Column objects formatted for the HTML table display
|
||||
|
||||
``display_rows``
|
||||
``display_rows`` - ``list``
|
||||
Row data formatted for the HTML table display
|
||||
|
||||
``foreign_key_tables``
|
||||
``foreign_key_tables`` - ``list``
|
||||
Tables that link to this row using foreign keys
|
||||
|
||||
``metadata``
|
||||
``metadata`` - ``dict``
|
||||
Metadata about the table, database or stored query
|
||||
|
||||
``ok``
|
||||
``ok`` - ``bool``
|
||||
True if the data for this page was retrieved without errors
|
||||
|
||||
``primary_key_values``
|
||||
``primary_key_values`` - ``list``
|
||||
Values of the primary keys for this row, from the URL
|
||||
|
||||
``primary_keys``
|
||||
``primary_keys`` - ``list``
|
||||
Primary keys for this table
|
||||
|
||||
``private``
|
||||
``private`` - ``bool``
|
||||
Whether this resource is private to the current actor
|
||||
|
||||
``query_ms``
|
||||
``query_ms`` - ``float``
|
||||
Time taken by the SQL queries for this page, in milliseconds
|
||||
|
||||
``renderers``
|
||||
``renderers`` - ``dict``
|
||||
Dictionary mapping output format names (e.g. json) to their URLs for this page
|
||||
|
||||
``row_actions``
|
||||
``row_actions`` - ``list``
|
||||
Row actions made available by plugin hooks
|
||||
|
||||
``rows``
|
||||
``rows`` - ``list``
|
||||
The rows for this page, as a list of dictionaries mapping column name to value
|
||||
|
||||
``select_templates``
|
||||
``select_templates`` - ``list``
|
||||
List of template names that were considered for this page, the one used marked with an asterisk
|
||||
|
||||
``settings``
|
||||
``settings`` - ``dict``
|
||||
Dictionary of Datasette's current settings
|
||||
|
||||
``table``
|
||||
``table`` - ``str``
|
||||
Table name
|
||||
|
||||
``top_row``
|
||||
``top_row`` - ``callable``
|
||||
Async function rendering the top_row plugin slot
|
||||
|
||||
``url_csv``
|
||||
``url_csv`` - ``str``
|
||||
URL for the CSV export of this page
|
||||
|
||||
``url_csv_hidden_args``
|
||||
``url_csv_hidden_args`` - ``list``
|
||||
(name, value) pairs for hidden form fields used by the CSV export form
|
||||
|
||||
``url_csv_path``
|
||||
``url_csv_path`` - ``str``
|
||||
Path portion of the CSV export URL
|
||||
|
||||
.. [[[end]]]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"""
|
||||
Cog helpers for generating docs/template_context.rst from the manifest
|
||||
in datasette/template_contexts.py - same pattern as json_api_doc.py.
|
||||
Cog helpers for generating docs/template_context.rst from the Context
|
||||
dataclasses and TEMPLATE_BASE_CONTEXT - same pattern as json_api_doc.py.
|
||||
"""
|
||||
|
||||
|
||||
def template_context(cog):
|
||||
from datasette.template_contexts import BASE_CONTEXT_KEYS, PAGES
|
||||
from datasette.app import TEMPLATE_BASE_CONTEXT
|
||||
from datasette.template_contexts import PAGES
|
||||
|
||||
cog.out("\n")
|
||||
_section(
|
||||
|
|
@ -19,36 +20,26 @@ def template_context(cog):
|
|||
":ref:`plugin_hook_extra_template_vars` hook."
|
||||
),
|
||||
)
|
||||
_untyped_keys(cog, BASE_CONTEXT_KEYS)
|
||||
for name, doc in TEMPLATE_BASE_CONTEXT.items():
|
||||
cog.out("``{}``\n".format(name))
|
||||
cog.out(" {}\n\n".format(doc))
|
||||
|
||||
for page in PAGES.values():
|
||||
_section(
|
||||
cog,
|
||||
"{} page".format(page.title),
|
||||
"{} Rendered using the ``{}`` template.".format(
|
||||
page.description, page.template
|
||||
),
|
||||
for klass in PAGES.values():
|
||||
title = "{} page".format(klass.__name__.removesuffix("Context"))
|
||||
intro = "{} Rendered using the ``{}`` template.".format(
|
||||
klass.__doc__, klass.template
|
||||
)
|
||||
if page.context_class is not None:
|
||||
for f in sorted(
|
||||
page.context_class.documented_fields(), key=lambda f: f.name
|
||||
):
|
||||
cog.out("``{}`` - ``{}``\n".format(f.name, f.type_name))
|
||||
cog.out(" {}\n\n".format(f.help))
|
||||
else:
|
||||
_section(cog, title, intro)
|
||||
if klass.extras_scope is not None:
|
||||
cog.out(
|
||||
"Many of these keys are shared with the :ref:`JSON API "
|
||||
"<json_api>` for this page.\n\n"
|
||||
)
|
||||
_untyped_keys(cog, page.documented_keys())
|
||||
for f in sorted(klass.documented_fields(), key=lambda f: f.name):
|
||||
cog.out("``{}`` - ``{}``\n".format(f.name, f.type_name))
|
||||
cog.out(" {}\n\n".format(f.help))
|
||||
|
||||
|
||||
def _section(cog, title, intro):
|
||||
cog.out("{}\n{}\n\n".format(title, "-" * len(title)))
|
||||
cog.out("{}\n\n".format(intro))
|
||||
|
||||
|
||||
def _untyped_keys(cog, keys):
|
||||
for key in keys:
|
||||
cog.out("``{}``\n".format(key.name))
|
||||
cog.out(" {}\n\n".format(key.doc))
|
||||
|
|
|
|||
|
|
@ -10,17 +10,11 @@ from dataclasses import dataclass, field
|
|||
|
||||
import pytest
|
||||
|
||||
from datasette.app import Datasette
|
||||
from datasette.app import Datasette, TEMPLATE_BASE_CONTEXT
|
||||
from datasette.extras import ExtraScope
|
||||
from datasette.fixtures import write_fixture_database
|
||||
from datasette.template_contexts import (
|
||||
BASE_CONTEXT_KEYS,
|
||||
PAGES,
|
||||
documented_context_keys,
|
||||
)
|
||||
from datasette.template_contexts import PAGES, documented_context_keys
|
||||
from datasette.views import Context
|
||||
from datasette.views.database import DatabaseContext, QueryContext
|
||||
from datasette.views.table_extras import table_extra_registry
|
||||
|
||||
|
||||
def test_documented_fields():
|
||||
|
|
@ -36,14 +30,20 @@ def test_documented_fields():
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("klass", (DatabaseContext, QueryContext))
|
||||
def test_context_dataclass_fields_all_have_help(klass):
|
||||
@pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__)
|
||||
def test_context_class_fields_all_have_help(klass):
|
||||
for context_field in klass.documented_fields():
|
||||
assert context_field.help, "{}.{} is missing help metadata".format(
|
||||
assert context_field.help, "{}.{} is missing documentation".format(
|
||||
klass.__name__, context_field.name
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__)
|
||||
def test_context_class_has_docstring_and_template(klass):
|
||||
assert klass.__doc__, "{} is missing a docstring".format(klass.__name__)
|
||||
assert klass.template, "{} is missing a template".format(klass.__name__)
|
||||
|
||||
|
||||
def test_extra_field_documentation_comes_from_the_extra_class():
|
||||
from datasette.views import extra_field
|
||||
from datasette.views.table_extras import CountExtra
|
||||
|
|
@ -169,8 +169,8 @@ async def test_template_context_matches_documented_contract(
|
|||
undocumented = actual - documented
|
||||
no_longer_present = documented - actual
|
||||
assert not undocumented, (
|
||||
"Undocumented keys in {} template context: {} - document them in "
|
||||
"datasette/template_contexts.py".format(page_name, sorted(undocumented))
|
||||
"Undocumented keys in {} template context: {} - add them to the "
|
||||
"page's Context class".format(page_name, sorted(undocumented))
|
||||
)
|
||||
assert not no_longer_present, (
|
||||
"Documented keys missing from {} template context: {} - this would "
|
||||
|
|
@ -178,53 +178,21 @@ async def test_template_context_matches_documented_contract(
|
|||
)
|
||||
|
||||
|
||||
def test_table_context_fields_match_documented_contract():
|
||||
from datasette.views.table import TableContext
|
||||
|
||||
assert {f.name for f in TableContext.documented_fields()} == {
|
||||
key.name for key in PAGES["table"].documented_keys()
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", PAGES.values(), ids=lambda page: page.name)
|
||||
def test_page_documented_keys_all_have_docs(page):
|
||||
for key in page.documented_keys():
|
||||
assert key.doc, "{} page key {} is missing docs".format(page.name, key.name)
|
||||
for name, doc in TEMPLATE_BASE_CONTEXT.items():
|
||||
assert doc, "Base context key {} is missing docs".format(name)
|
||||
|
||||
|
||||
def test_template_context_docs_cover_every_documented_key():
|
||||
docs_path = pathlib.Path(__file__).parent.parent / "docs" / "template_context.rst"
|
||||
assert docs_path.exists(), "docs/template_context.rst is missing"
|
||||
docs = docs_path.read_text()
|
||||
for key in BASE_CONTEXT_KEYS:
|
||||
assert "``{}``".format(key.name) in docs, key.name
|
||||
for page in PAGES.values():
|
||||
assert page.title in docs, page.title
|
||||
for key in page.documented_keys():
|
||||
assert "``{}``".format(key.name) in docs, "{} ({} page)".format(
|
||||
key.name, page.name
|
||||
for name in TEMPLATE_BASE_CONTEXT:
|
||||
assert "``{}``".format(name) in docs, name
|
||||
for page_name, klass in PAGES.items():
|
||||
title = "{} page".format(klass.__name__.removesuffix("Context"))
|
||||
assert title in docs, title
|
||||
for context_field in klass.documented_fields():
|
||||
assert "``{}``".format(context_field.name) in docs, "{} ({} page)".format(
|
||||
context_field.name, page_name
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("page", PAGES.values(), ids=lambda page: page.name)
|
||||
def test_page_extra_keys_are_registered_extras(page):
|
||||
for name in page.extra_keys:
|
||||
cls = table_extra_registry.classes_by_name.get(name)
|
||||
assert cls is not None, "{} is not a registered extra".format(name)
|
||||
assert page.extras_scope is not None
|
||||
assert cls.available_for(
|
||||
page.extras_scope
|
||||
), "{} extra is not available for scope {}".format(name, page.extras_scope)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue