mirror of
https://github.com/simonw/datasette.git
synced 2026-06-11 19:46:58 +02:00
Refactor table JSON extras into classes
This commit is contained in:
parent
03f1ffdf8f
commit
17bbe6855c
3 changed files with 884 additions and 540 deletions
|
|
@ -1,6 +1,100 @@
|
|||
import re
|
||||
from enum import Enum
|
||||
from typing import ClassVar
|
||||
|
||||
from asyncinject import Registry
|
||||
|
||||
|
||||
def extra_names_from_request(request):
|
||||
extra_bits = request.args.getlist("_extra")
|
||||
extras = set()
|
||||
for bit in extra_bits:
|
||||
extras.update(part for part in bit.split(",") if part)
|
||||
return extras
|
||||
|
||||
|
||||
class ExtraScope(Enum):
|
||||
TABLE = "table"
|
||||
|
||||
|
||||
class Provider:
|
||||
name: ClassVar[str | None] = None
|
||||
scopes: ClassVar[frozenset[ExtraScope]] = frozenset()
|
||||
public: ClassVar[bool] = False
|
||||
|
||||
@classmethod
|
||||
def key(cls):
|
||||
return cls.name or _camel_to_snake(cls.__name__)
|
||||
|
||||
@classmethod
|
||||
def available_for(cls, scope):
|
||||
return scope in cls.scopes
|
||||
|
||||
async def resolve(self, context):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Extra(Provider):
|
||||
description: ClassVar[str | None] = None
|
||||
public: ClassVar[bool] = True
|
||||
stable: ClassVar[bool] = True
|
||||
expensive: ClassVar[bool] = False
|
||||
docs_note: ClassVar[str | None] = None
|
||||
|
||||
@classmethod
|
||||
def documentation(cls):
|
||||
return {
|
||||
"name": cls.key(),
|
||||
"description": cls.description,
|
||||
"scopes": [
|
||||
scope.value for scope in sorted(cls.scopes, key=lambda s: s.value)
|
||||
],
|
||||
"stable": cls.stable,
|
||||
"expensive": cls.expensive,
|
||||
"docs_note": cls.docs_note,
|
||||
}
|
||||
|
||||
|
||||
class ExtraRegistry:
|
||||
def __init__(self, classes):
|
||||
self.classes = list(classes)
|
||||
self.classes_by_name = {cls.key(): cls for cls in self.classes}
|
||||
|
||||
def classes_for_scope(self, scope, include_internal=True):
|
||||
classes = [
|
||||
cls
|
||||
for cls in self.classes
|
||||
if cls.available_for(scope) and (include_internal or cls.public)
|
||||
]
|
||||
return classes
|
||||
|
||||
def public_classes_for_scope(self, scope):
|
||||
return self.classes_for_scope(scope, include_internal=False)
|
||||
|
||||
async def resolve(self, requested, context, scope):
|
||||
registry = Registry()
|
||||
|
||||
async def context_provider():
|
||||
return context
|
||||
|
||||
registry.register(context_provider, name="context")
|
||||
|
||||
for cls in self.classes_for_scope(scope):
|
||||
registry.register(cls().resolve, name=cls.key())
|
||||
|
||||
public_names = {cls.key() for cls in self.public_classes_for_scope(scope)}
|
||||
requested_public_names = [
|
||||
name
|
||||
for name in requested
|
||||
if name in public_names and name in registry._registry
|
||||
]
|
||||
resolved = await registry.resolve_multi(requested_public_names)
|
||||
return {
|
||||
name: resolved[name] for name in requested_public_names if name in resolved
|
||||
}
|
||||
|
||||
|
||||
def _camel_to_snake(name):
|
||||
name = re.sub(r"(Extra|Provider)$", "", name)
|
||||
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
||||
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@ import itertools
|
|||
import json
|
||||
import urllib
|
||||
|
||||
from asyncinject import Registry
|
||||
import markupsafe
|
||||
|
||||
from datasette.extras import extra_names_from_request
|
||||
from datasette.plugins import pm
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette.events import (
|
||||
AlterTableEvent,
|
||||
DropTableEvent,
|
||||
|
|
@ -47,6 +45,12 @@ from datasette.filters import Filters
|
|||
import sqlite_utils
|
||||
from .base import BaseView, DatasetteError, _error, stream_csv
|
||||
from .database import QueryView
|
||||
from .table_extras import (
|
||||
TABLE_EXTRA_BUNDLES,
|
||||
TableExtraContext,
|
||||
resolve_table_extras,
|
||||
table_extra_registry,
|
||||
)
|
||||
|
||||
LINK_WITH_LABEL = (
|
||||
'<a href="{base_url}{database}/{table}/{link_id}">{label}</a> <em>{id}</em>'
|
||||
|
|
@ -1465,560 +1469,60 @@ async def table_view_data(
|
|||
if extra_extras:
|
||||
extras.update(extra_extras)
|
||||
|
||||
async def extra_count_sql():
|
||||
return count_sql
|
||||
|
||||
async def extra_count():
|
||||
"Total count of rows matching these filters"
|
||||
# Calculate the total count for this query
|
||||
count = None
|
||||
if (
|
||||
not db.is_mutable
|
||||
and datasette.inspect_data
|
||||
and count_sql == f"select count(*) from {table_name} "
|
||||
):
|
||||
# We can use a previously cached table row count
|
||||
try:
|
||||
count = datasette.inspect_data[database_name]["tables"][table_name][
|
||||
"count"
|
||||
]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Otherwise run a select count(*) ...
|
||||
if count_sql and count is None and not nocount:
|
||||
count_sql_limited = (
|
||||
f"select count(*) from (select * {from_sql} limit 10001)"
|
||||
)
|
||||
try:
|
||||
count_rows = list(await db.execute(count_sql_limited, from_sql_params))
|
||||
count = count_rows[0][0]
|
||||
except QueryInterrupted:
|
||||
pass
|
||||
return count
|
||||
|
||||
async def facet_instances(extra_count):
|
||||
facet_instances = []
|
||||
facet_classes = list(
|
||||
itertools.chain.from_iterable(pm.hook.register_facet_classes())
|
||||
)
|
||||
for facet_class in facet_classes:
|
||||
facet_instances.append(
|
||||
facet_class(
|
||||
datasette,
|
||||
request,
|
||||
database_name,
|
||||
sql=sql_no_order_no_limit,
|
||||
params=params,
|
||||
table=table_name,
|
||||
table_config=table_metadata,
|
||||
row_count=extra_count,
|
||||
)
|
||||
)
|
||||
return facet_instances
|
||||
|
||||
async def extra_facet_results(facet_instances):
|
||||
"Results of facets calculated against this data"
|
||||
facet_results = {}
|
||||
facets_timed_out = []
|
||||
|
||||
if not nofacet:
|
||||
# Run them in parallel
|
||||
facet_awaitables = [facet.facet_results() for facet in facet_instances]
|
||||
facet_awaitable_results = await run_sequential(*facet_awaitables)
|
||||
for (
|
||||
instance_facet_results,
|
||||
instance_facets_timed_out,
|
||||
) in facet_awaitable_results:
|
||||
for facet_info in instance_facet_results:
|
||||
base_key = facet_info["name"]
|
||||
key = base_key
|
||||
i = 1
|
||||
while key in facet_results:
|
||||
i += 1
|
||||
key = f"{base_key}_{i}"
|
||||
facet_results[key] = facet_info
|
||||
facets_timed_out.extend(instance_facets_timed_out)
|
||||
|
||||
return {
|
||||
"results": facet_results,
|
||||
"timed_out": facets_timed_out,
|
||||
}
|
||||
|
||||
async def extra_suggested_facets(facet_instances):
|
||||
"Suggestions for facets that might return interesting results"
|
||||
suggested_facets = []
|
||||
# Calculate suggested facets
|
||||
if (
|
||||
datasette.setting("suggest_facets")
|
||||
and datasette.setting("allow_facet")
|
||||
and not _next
|
||||
and not nofacet
|
||||
and not nosuggest
|
||||
):
|
||||
# Run them in parallel
|
||||
facet_suggest_awaitables = [facet.suggest() for facet in facet_instances]
|
||||
for suggest_result in await run_sequential(*facet_suggest_awaitables):
|
||||
suggested_facets.extend(suggest_result)
|
||||
return suggested_facets
|
||||
|
||||
# Faceting
|
||||
if not datasette.setting("allow_facet") and any(
|
||||
arg.startswith("_facet") for arg in request.args
|
||||
):
|
||||
raise BadRequest("_facet= is not allowed")
|
||||
|
||||
# human_description_en combines filters AND search, if provided
|
||||
async def extra_human_description_en():
|
||||
"Human-readable description of the filters"
|
||||
human_description_en = filters.human_description_en(
|
||||
extra=extra_human_descriptions
|
||||
)
|
||||
if sort or sort_desc:
|
||||
human_description_en = " ".join(
|
||||
[b for b in [human_description_en, sorted_by] if b]
|
||||
)
|
||||
return human_description_en
|
||||
|
||||
if sort or sort_desc:
|
||||
sorted_by = "sorted by {}{}".format(
|
||||
(sort or sort_desc), " descending" if sort_desc else ""
|
||||
)
|
||||
|
||||
async def extra_next_url():
|
||||
"Full URL for the next page of results"
|
||||
return next_url
|
||||
|
||||
async def extra_columns():
|
||||
"Column names returned by this query"
|
||||
return columns
|
||||
|
||||
async def extra_all_columns():
|
||||
"All columns in the table, regardless of _col/_nocol filtering"
|
||||
return list(table_columns)
|
||||
|
||||
async def extra_primary_keys():
|
||||
"Primary keys for this table"
|
||||
return pks
|
||||
|
||||
async def extra_actions():
|
||||
async def actions():
|
||||
links = []
|
||||
kwargs = {
|
||||
"datasette": datasette,
|
||||
"database": database_name,
|
||||
"actor": request.actor,
|
||||
"request": request,
|
||||
}
|
||||
if is_view:
|
||||
kwargs["view"] = table_name
|
||||
method = pm.hook.view_actions
|
||||
else:
|
||||
kwargs["table"] = table_name
|
||||
method = pm.hook.table_actions
|
||||
for hook in method(**kwargs):
|
||||
extra_links = await await_me_maybe(hook)
|
||||
if extra_links:
|
||||
links.extend(extra_links)
|
||||
return links
|
||||
|
||||
return actions
|
||||
|
||||
async def extra_is_view():
|
||||
return is_view
|
||||
|
||||
async def extra_debug():
|
||||
"Extra debug information"
|
||||
return {
|
||||
"resolved": repr(resolved),
|
||||
"url_vars": request.url_vars,
|
||||
"nofacet": nofacet,
|
||||
"nosuggest": nosuggest,
|
||||
}
|
||||
|
||||
async def extra_request():
|
||||
"Full information about the request"
|
||||
return {
|
||||
"url": request.url,
|
||||
"path": request.path,
|
||||
"full_path": request.full_path,
|
||||
"host": request.host,
|
||||
"args": request.args._data,
|
||||
}
|
||||
|
||||
async def run_display_columns_and_rows():
|
||||
display_columns, display_rows = await display_columns_and_rows(
|
||||
datasette,
|
||||
database_name,
|
||||
table_name,
|
||||
results.description,
|
||||
rows,
|
||||
link_column=not is_view,
|
||||
truncate_cells=datasette.setting("truncate_cells_html"),
|
||||
sortable_columns=sortable_columns,
|
||||
request=request,
|
||||
)
|
||||
return {
|
||||
"columns": display_columns,
|
||||
"rows": display_rows,
|
||||
}
|
||||
|
||||
async def extra_display_columns(run_display_columns_and_rows):
|
||||
return run_display_columns_and_rows["columns"]
|
||||
|
||||
async def extra_display_rows(run_display_columns_and_rows):
|
||||
return run_display_columns_and_rows["rows"]
|
||||
|
||||
async def extra_render_cell():
|
||||
"Rendered HTML for each cell using the render_cell plugin hook"
|
||||
pks_for_display = pks if pks else (["rowid"] if not is_view else [])
|
||||
col_names = [col[0] for col in results.description]
|
||||
ct_map = await datasette.get_column_types(database_name, table_name)
|
||||
rendered_rows = []
|
||||
for row in rows:
|
||||
rendered_row = {}
|
||||
for value, column in zip(row, col_names):
|
||||
ct = ct_map.get(column)
|
||||
plugin_display_value = None
|
||||
# Try column type render_cell first
|
||||
if ct:
|
||||
candidate = await ct.render_cell(
|
||||
value=value,
|
||||
column=column,
|
||||
table=table_name,
|
||||
database=database_name,
|
||||
datasette=datasette,
|
||||
request=request,
|
||||
)
|
||||
if candidate is not None:
|
||||
plugin_display_value = candidate
|
||||
if plugin_display_value is None:
|
||||
for candidate in pm.hook.render_cell(
|
||||
row=row,
|
||||
value=value,
|
||||
column=column,
|
||||
table=table_name,
|
||||
pks=pks_for_display,
|
||||
database=database_name,
|
||||
datasette=datasette,
|
||||
request=request,
|
||||
column_type=ct,
|
||||
):
|
||||
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)
|
||||
rendered_rows.append(rendered_row)
|
||||
return rendered_rows
|
||||
|
||||
async def extra_query():
|
||||
"Details of the underlying SQL query"
|
||||
return {
|
||||
"sql": sql,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
async def extra_column_types():
|
||||
"Column type assignments for this table"
|
||||
ct_map = await datasette.get_column_types(database_name, table_name)
|
||||
return {
|
||||
col_name: {
|
||||
"type": ct.name,
|
||||
"config": ct.config,
|
||||
}
|
||||
for col_name, ct in ct_map.items()
|
||||
}
|
||||
|
||||
async def extra_set_column_type_ui():
|
||||
"Column type UI metadata for this table"
|
||||
if is_view:
|
||||
return None
|
||||
|
||||
if not await datasette.allowed(
|
||||
action="set-column-type",
|
||||
resource=TableResource(database=database_name, table=table_name),
|
||||
actor=request.actor,
|
||||
):
|
||||
return None
|
||||
|
||||
column_details = await datasette._get_resource_column_details(
|
||||
database_name, table_name
|
||||
)
|
||||
ct_map = await datasette.get_column_types(database_name, table_name)
|
||||
columns = {}
|
||||
for column_name, column_detail in column_details.items():
|
||||
current = ct_map.get(column_name)
|
||||
columns[column_name] = {
|
||||
"current": (
|
||||
{"type": current.name, "config": current.config}
|
||||
if current is not None
|
||||
else None
|
||||
),
|
||||
"options": [
|
||||
{
|
||||
"name": name,
|
||||
"description": ct_cls.description,
|
||||
}
|
||||
for name, ct_cls in sorted(datasette._column_types.items())
|
||||
if datasette._column_type_is_applicable(ct_cls, column_detail)
|
||||
],
|
||||
}
|
||||
return {
|
||||
"path": "{}/-/set-column-type".format(
|
||||
datasette.urls.table(database_name, table_name)
|
||||
),
|
||||
"columns": columns,
|
||||
}
|
||||
|
||||
async def extra_metadata():
|
||||
"Metadata about the table and database"
|
||||
tablemetadata = await datasette.get_resource_metadata(database_name, table_name)
|
||||
|
||||
rows = await datasette.get_internal_database().execute(
|
||||
"""
|
||||
SELECT
|
||||
column_name,
|
||||
value
|
||||
FROM metadata_columns
|
||||
WHERE database_name = ?
|
||||
AND resource_name = ?
|
||||
AND key = 'description'
|
||||
""",
|
||||
[database_name, table_name],
|
||||
)
|
||||
tablemetadata["columns"] = dict(rows)
|
||||
return tablemetadata
|
||||
|
||||
async def extra_database():
|
||||
return database_name
|
||||
|
||||
async def extra_table():
|
||||
return table_name
|
||||
|
||||
async def extra_database_color():
|
||||
return db.color
|
||||
|
||||
async def extra_form_hidden_args():
|
||||
form_hidden_args = []
|
||||
for key in request.args:
|
||||
if (
|
||||
key.startswith("_")
|
||||
and key not in ("_sort", "_sort_desc", "_search", "_next")
|
||||
and "__" not in key
|
||||
):
|
||||
for value in request.args.getlist(key):
|
||||
form_hidden_args.append((key, value))
|
||||
return form_hidden_args
|
||||
|
||||
async def extra_filters():
|
||||
return filters
|
||||
|
||||
async def extra_custom_table_templates():
|
||||
return [
|
||||
f"_table-{to_css_class(database_name)}-{to_css_class(table_name)}.html",
|
||||
f"_table-table-{to_css_class(database_name)}-{to_css_class(table_name)}.html",
|
||||
"_table.html",
|
||||
]
|
||||
|
||||
async def extra_sorted_facet_results(extra_facet_results):
|
||||
facet_configs = table_metadata.get("facets", [])
|
||||
if facet_configs:
|
||||
# Build ordered list of facet names from metadata config
|
||||
metadata_facet_names = []
|
||||
for fc in facet_configs:
|
||||
if isinstance(fc, str):
|
||||
metadata_facet_names.append(fc)
|
||||
elif isinstance(fc, dict):
|
||||
metadata_facet_names.append(list(fc.values())[0])
|
||||
metadata_order = {name: i for i, name in enumerate(metadata_facet_names)}
|
||||
metadata_facets = []
|
||||
request_facets = []
|
||||
for f in extra_facet_results["results"].values():
|
||||
if f["name"] in metadata_order:
|
||||
metadata_facets.append(f)
|
||||
else:
|
||||
request_facets.append(f)
|
||||
metadata_facets.sort(key=lambda f: metadata_order[f["name"]])
|
||||
request_facets.sort(
|
||||
key=lambda f: (len(f["results"]), f["name"]),
|
||||
reverse=True,
|
||||
)
|
||||
return metadata_facets + request_facets
|
||||
else:
|
||||
return sorted(
|
||||
extra_facet_results["results"].values(),
|
||||
key=lambda f: (len(f["results"]), f["name"]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
async def extra_table_definition():
|
||||
return await db.get_table_definition(table_name)
|
||||
|
||||
async def extra_view_definition():
|
||||
return await db.get_view_definition(table_name)
|
||||
|
||||
async def extra_renderers(extra_expandable_columns, extra_query):
|
||||
renderers = {}
|
||||
url_labels_extra = {}
|
||||
if extra_expandable_columns:
|
||||
url_labels_extra = {"_labels": "on"}
|
||||
for key, (_, can_render) in datasette.renderers.items():
|
||||
it_can_render = call_with_supported_arguments(
|
||||
can_render,
|
||||
datasette=datasette,
|
||||
columns=columns or [],
|
||||
rows=rows or [],
|
||||
sql=extra_query.get("sql", None),
|
||||
query_name=None,
|
||||
database=database_name,
|
||||
table=table_name,
|
||||
request=request,
|
||||
view_name="table",
|
||||
)
|
||||
it_can_render = await await_me_maybe(it_can_render)
|
||||
if it_can_render:
|
||||
renderers[key] = datasette.urls.path(
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format=key,
|
||||
extra_qs={**url_labels_extra},
|
||||
)
|
||||
)
|
||||
return renderers
|
||||
|
||||
async def extra_private():
|
||||
return private
|
||||
|
||||
async def extra_expandable_columns():
|
||||
expandables = []
|
||||
db = datasette.databases[database_name]
|
||||
for fk in await db.foreign_keys_for_table(table_name):
|
||||
label_column = await db.label_column_for_table(fk["other_table"])
|
||||
expandables.append((fk, label_column))
|
||||
return expandables
|
||||
|
||||
async def extra_extras():
|
||||
"Available ?_extra= blocks"
|
||||
all_extras = [
|
||||
(key[len("extra_") :], fn.__doc__)
|
||||
for key, fn in registry._registry.items()
|
||||
if key.startswith("extra_")
|
||||
]
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
"description": doc,
|
||||
"toggle_url": datasette.absolute_url(
|
||||
request,
|
||||
datasette.urls.path(
|
||||
path_with_added_args(request, {"_extra": name})
|
||||
if name not in extras
|
||||
else path_with_removed_args(request, {"_extra": name})
|
||||
),
|
||||
),
|
||||
"selected": name in extras,
|
||||
}
|
||||
for name, doc in all_extras
|
||||
]
|
||||
|
||||
async def extra_facets_timed_out(extra_facet_results):
|
||||
return extra_facet_results["timed_out"]
|
||||
|
||||
bundles = {
|
||||
"html": [
|
||||
"suggested_facets",
|
||||
"facet_results",
|
||||
"facets_timed_out",
|
||||
"count",
|
||||
"count_sql",
|
||||
"human_description_en",
|
||||
"next_url",
|
||||
"metadata",
|
||||
"query",
|
||||
"columns",
|
||||
"display_columns",
|
||||
"display_rows",
|
||||
"database",
|
||||
"table",
|
||||
"database_color",
|
||||
"actions",
|
||||
"filters",
|
||||
"renderers",
|
||||
"custom_table_templates",
|
||||
"sorted_facet_results",
|
||||
"table_definition",
|
||||
"view_definition",
|
||||
"is_view",
|
||||
"private",
|
||||
"primary_keys",
|
||||
"all_columns",
|
||||
"expandable_columns",
|
||||
"form_hidden_args",
|
||||
"set_column_type_ui",
|
||||
]
|
||||
}
|
||||
|
||||
for key, values in bundles.items():
|
||||
for key, values in TABLE_EXTRA_BUNDLES.items():
|
||||
if f"_{key}" in extras:
|
||||
extras.update(values)
|
||||
extras.discard(f"_{key}")
|
||||
|
||||
registry = Registry(
|
||||
extra_count,
|
||||
extra_count_sql,
|
||||
extra_facet_results,
|
||||
extra_facets_timed_out,
|
||||
extra_suggested_facets,
|
||||
facet_instances,
|
||||
extra_human_description_en,
|
||||
extra_next_url,
|
||||
extra_columns,
|
||||
extra_all_columns,
|
||||
extra_primary_keys,
|
||||
run_display_columns_and_rows,
|
||||
extra_display_columns,
|
||||
extra_display_rows,
|
||||
extra_render_cell,
|
||||
extra_debug,
|
||||
extra_request,
|
||||
extra_query,
|
||||
extra_column_types,
|
||||
extra_set_column_type_ui,
|
||||
extra_metadata,
|
||||
extra_extras,
|
||||
extra_database,
|
||||
extra_table,
|
||||
extra_database_color,
|
||||
extra_actions,
|
||||
extra_filters,
|
||||
extra_renderers,
|
||||
extra_custom_table_templates,
|
||||
extra_sorted_facet_results,
|
||||
extra_table_definition,
|
||||
extra_view_definition,
|
||||
extra_is_view,
|
||||
extra_private,
|
||||
extra_expandable_columns,
|
||||
extra_form_hidden_args,
|
||||
table_extra_context = TableExtraContext(
|
||||
datasette=datasette,
|
||||
request=request,
|
||||
resolved=resolved,
|
||||
db=db,
|
||||
database_name=database_name,
|
||||
table_name=table_name,
|
||||
is_view=is_view,
|
||||
private=private,
|
||||
rows=rows,
|
||||
columns=columns,
|
||||
results_description=results.description,
|
||||
table_columns=table_columns,
|
||||
pks=pks,
|
||||
count_sql=count_sql,
|
||||
from_sql=from_sql,
|
||||
from_sql_params=from_sql_params,
|
||||
nocount=nocount,
|
||||
nofacet=nofacet,
|
||||
nosuggest=nosuggest,
|
||||
next_arg=request.args.get("_next"),
|
||||
next_value=next_value,
|
||||
next_url=next_url,
|
||||
sql=sql,
|
||||
sql_no_order_no_limit=sql_no_order_no_limit,
|
||||
params=params,
|
||||
table_metadata=table_metadata,
|
||||
filters=filters,
|
||||
extra_human_descriptions=extra_human_descriptions,
|
||||
sort=sort,
|
||||
sort_desc=sort_desc,
|
||||
sortable_columns=sortable_columns,
|
||||
extras=extras,
|
||||
extra_registry=table_extra_registry,
|
||||
display_columns_and_rows=display_columns_and_rows,
|
||||
run_sequential=run_sequential,
|
||||
)
|
||||
|
||||
results = await registry.resolve_multi(
|
||||
["extra_{}".format(extra) for extra in extras]
|
||||
)
|
||||
data = {
|
||||
"ok": True,
|
||||
"next": next_value and str(next_value) or None,
|
||||
}
|
||||
data.update(
|
||||
{
|
||||
key.replace("extra_", ""): value
|
||||
for key, value in results.items()
|
||||
if key.startswith("extra_") and key.replace("extra_", "") in extras
|
||||
}
|
||||
)
|
||||
data.update(await resolve_table_extras(extras, table_extra_context))
|
||||
raw_sqlite_rows = rows[:page_size]
|
||||
# Apply transform_value for columns with assigned types
|
||||
ct_map = await datasette.get_column_types(database_name, table_name)
|
||||
|
|
|
|||
746
datasette/views/table_extras.py
Normal file
746
datasette/views/table_extras.py
Normal file
|
|
@ -0,0 +1,746 @@
|
|||
import itertools
|
||||
from dataclasses import dataclass
|
||||
|
||||
from datasette.database import QueryInterrupted
|
||||
from datasette.extras import Extra, ExtraRegistry, ExtraScope, Provider
|
||||
from datasette.plugins import pm
|
||||
from datasette.resources import TableResource
|
||||
from datasette.utils import (
|
||||
await_me_maybe,
|
||||
call_with_supported_arguments,
|
||||
path_with_added_args,
|
||||
path_with_format,
|
||||
path_with_removed_args,
|
||||
to_css_class,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TableExtraContext:
|
||||
datasette: object
|
||||
request: object
|
||||
resolved: object
|
||||
db: object
|
||||
database_name: str
|
||||
table_name: str
|
||||
is_view: bool
|
||||
private: bool
|
||||
rows: list
|
||||
columns: list
|
||||
results_description: list
|
||||
table_columns: list
|
||||
pks: list
|
||||
count_sql: str
|
||||
from_sql: str
|
||||
from_sql_params: dict
|
||||
nocount: object
|
||||
nofacet: object
|
||||
nosuggest: object
|
||||
next_arg: object
|
||||
next_value: object
|
||||
next_url: str | None
|
||||
sql: str
|
||||
sql_no_order_no_limit: str
|
||||
params: dict
|
||||
table_metadata: dict
|
||||
filters: object
|
||||
extra_human_descriptions: list
|
||||
sort: str | None
|
||||
sort_desc: str | None
|
||||
sortable_columns: set
|
||||
extras: set
|
||||
extra_registry: ExtraRegistry
|
||||
display_columns_and_rows: object
|
||||
run_sequential: object
|
||||
|
||||
|
||||
class CountSqlExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.count_sql
|
||||
|
||||
|
||||
class CountExtra(Extra):
|
||||
description = "Total count of rows matching these filters"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
expensive = True
|
||||
|
||||
async def resolve(self, context):
|
||||
count = None
|
||||
if (
|
||||
not context.db.is_mutable
|
||||
and context.datasette.inspect_data
|
||||
and context.count_sql == f"select count(*) from {context.table_name} "
|
||||
):
|
||||
try:
|
||||
count = context.datasette.inspect_data[context.database_name]["tables"][
|
||||
context.table_name
|
||||
]["count"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if context.count_sql and count is None and not context.nocount:
|
||||
count_sql_limited = (
|
||||
f"select count(*) from (select * {context.from_sql} limit 10001)"
|
||||
)
|
||||
try:
|
||||
count_rows = list(
|
||||
await context.db.execute(count_sql_limited, context.from_sql_params)
|
||||
)
|
||||
count = count_rows[0][0]
|
||||
except QueryInterrupted:
|
||||
pass
|
||||
return count
|
||||
|
||||
|
||||
class FacetInstancesProvider(Provider):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context, count):
|
||||
facet_instances = []
|
||||
facet_classes = list(
|
||||
itertools.chain.from_iterable(pm.hook.register_facet_classes())
|
||||
)
|
||||
for facet_class in facet_classes:
|
||||
facet_instances.append(
|
||||
facet_class(
|
||||
context.datasette,
|
||||
context.request,
|
||||
context.database_name,
|
||||
sql=context.sql_no_order_no_limit,
|
||||
params=context.params,
|
||||
table=context.table_name,
|
||||
table_config=context.table_metadata,
|
||||
row_count=count,
|
||||
)
|
||||
)
|
||||
return facet_instances
|
||||
|
||||
|
||||
class FacetResultsExtra(Extra):
|
||||
description = "Results of facets calculated against this data"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
expensive = True
|
||||
|
||||
async def resolve(self, context, facet_instances):
|
||||
facet_results = {}
|
||||
facets_timed_out = []
|
||||
|
||||
if not context.nofacet:
|
||||
facet_awaitables = [facet.facet_results() for facet in facet_instances]
|
||||
facet_awaitable_results = await context.run_sequential(*facet_awaitables)
|
||||
for (
|
||||
instance_facet_results,
|
||||
instance_facets_timed_out,
|
||||
) in facet_awaitable_results:
|
||||
for facet_info in instance_facet_results:
|
||||
base_key = facet_info["name"]
|
||||
key = base_key
|
||||
i = 1
|
||||
while key in facet_results:
|
||||
i += 1
|
||||
key = f"{base_key}_{i}"
|
||||
facet_results[key] = facet_info
|
||||
facets_timed_out.extend(instance_facets_timed_out)
|
||||
|
||||
return {
|
||||
"results": facet_results,
|
||||
"timed_out": facets_timed_out,
|
||||
}
|
||||
|
||||
|
||||
class FacetsTimedOutExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context, facet_results):
|
||||
return facet_results["timed_out"]
|
||||
|
||||
|
||||
class SuggestedFacetsExtra(Extra):
|
||||
description = "Suggestions for facets that might return interesting results"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
expensive = True
|
||||
|
||||
async def resolve(self, context, facet_instances):
|
||||
suggested_facets = []
|
||||
if (
|
||||
context.datasette.setting("suggest_facets")
|
||||
and context.datasette.setting("allow_facet")
|
||||
and not context.next_arg
|
||||
and not context.nofacet
|
||||
and not context.nosuggest
|
||||
):
|
||||
facet_suggest_awaitables = [facet.suggest() for facet in facet_instances]
|
||||
for suggest_result in await context.run_sequential(
|
||||
*facet_suggest_awaitables
|
||||
):
|
||||
suggested_facets.extend(suggest_result)
|
||||
return suggested_facets
|
||||
|
||||
|
||||
class HumanDescriptionEnExtra(Extra):
|
||||
description = "Human-readable description of the filters"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
human_description_en = context.filters.human_description_en(
|
||||
extra=context.extra_human_descriptions
|
||||
)
|
||||
if context.sort or context.sort_desc:
|
||||
sorted_by = "sorted by {}{}".format(
|
||||
(context.sort or context.sort_desc),
|
||||
" descending" if context.sort_desc else "",
|
||||
)
|
||||
human_description_en = " ".join(
|
||||
[b for b in [human_description_en, sorted_by] if b]
|
||||
)
|
||||
return human_description_en
|
||||
|
||||
|
||||
class NextUrlExtra(Extra):
|
||||
description = "Full URL for the next page of results"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.next_url
|
||||
|
||||
|
||||
class ColumnsExtra(Extra):
|
||||
description = "Column names returned by this query"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.columns
|
||||
|
||||
|
||||
class AllColumnsExtra(Extra):
|
||||
description = "All columns in the table, regardless of _col/_nocol filtering"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return list(context.table_columns)
|
||||
|
||||
|
||||
class PrimaryKeysExtra(Extra):
|
||||
description = "Primary keys for this table"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.pks
|
||||
|
||||
|
||||
class ActionsExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
async def actions():
|
||||
links = []
|
||||
kwargs = {
|
||||
"datasette": context.datasette,
|
||||
"database": context.database_name,
|
||||
"actor": context.request.actor,
|
||||
"request": context.request,
|
||||
}
|
||||
if context.is_view:
|
||||
kwargs["view"] = context.table_name
|
||||
method = pm.hook.view_actions
|
||||
else:
|
||||
kwargs["table"] = context.table_name
|
||||
method = pm.hook.table_actions
|
||||
for hook in method(**kwargs):
|
||||
extra_links = await await_me_maybe(hook)
|
||||
if extra_links:
|
||||
links.extend(extra_links)
|
||||
return links
|
||||
|
||||
return actions
|
||||
|
||||
|
||||
class IsViewExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.is_view
|
||||
|
||||
|
||||
class DebugExtra(Extra):
|
||||
description = "Extra debug information"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return {
|
||||
"resolved": repr(context.resolved),
|
||||
"url_vars": context.request.url_vars,
|
||||
"nofacet": context.nofacet,
|
||||
"nosuggest": context.nosuggest,
|
||||
}
|
||||
|
||||
|
||||
class RequestExtra(Extra):
|
||||
description = "Full information about the request"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return {
|
||||
"url": context.request.url,
|
||||
"path": context.request.path,
|
||||
"full_path": context.request.full_path,
|
||||
"host": context.request.host,
|
||||
"args": context.request.args._data,
|
||||
}
|
||||
|
||||
|
||||
class DisplayColumnsAndRowsProvider(Provider):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
display_columns, display_rows = await context.display_columns_and_rows(
|
||||
context.datasette,
|
||||
context.database_name,
|
||||
context.table_name,
|
||||
context.results_description,
|
||||
context.rows,
|
||||
link_column=not context.is_view,
|
||||
truncate_cells=context.datasette.setting("truncate_cells_html"),
|
||||
sortable_columns=context.sortable_columns,
|
||||
request=context.request,
|
||||
)
|
||||
return {
|
||||
"columns": display_columns,
|
||||
"rows": display_rows,
|
||||
}
|
||||
|
||||
|
||||
class DisplayColumnsExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context, display_columns_and_rows):
|
||||
return display_columns_and_rows["columns"]
|
||||
|
||||
|
||||
class DisplayRowsExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context, display_columns_and_rows):
|
||||
return display_columns_and_rows["rows"]
|
||||
|
||||
|
||||
class RenderCellExtra(Extra):
|
||||
description = "Rendered HTML for each cell using the render_cell plugin hook"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
pks_for_display = (
|
||||
context.pks if context.pks else (["rowid"] if not context.is_view else [])
|
||||
)
|
||||
col_names = [col[0] for col in context.results_description]
|
||||
ct_map = await context.datasette.get_column_types(
|
||||
context.database_name, context.table_name
|
||||
)
|
||||
rendered_rows = []
|
||||
for row in context.rows:
|
||||
rendered_row = {}
|
||||
for value, column in zip(row, col_names):
|
||||
ct = ct_map.get(column)
|
||||
plugin_display_value = None
|
||||
if ct:
|
||||
candidate = await ct.render_cell(
|
||||
value=value,
|
||||
column=column,
|
||||
table=context.table_name,
|
||||
database=context.database_name,
|
||||
datasette=context.datasette,
|
||||
request=context.request,
|
||||
)
|
||||
if candidate is not None:
|
||||
plugin_display_value = candidate
|
||||
if plugin_display_value is None:
|
||||
for candidate in pm.hook.render_cell(
|
||||
row=row,
|
||||
value=value,
|
||||
column=column,
|
||||
table=context.table_name,
|
||||
pks=pks_for_display,
|
||||
database=context.database_name,
|
||||
datasette=context.datasette,
|
||||
request=context.request,
|
||||
column_type=ct,
|
||||
):
|
||||
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)
|
||||
rendered_rows.append(rendered_row)
|
||||
return rendered_rows
|
||||
|
||||
|
||||
class QueryExtra(Extra):
|
||||
description = "Details of the underlying SQL query"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return {
|
||||
"sql": context.sql,
|
||||
"params": context.params,
|
||||
}
|
||||
|
||||
|
||||
class ColumnTypesExtra(Extra):
|
||||
description = "Column type assignments for this table"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
ct_map = await context.datasette.get_column_types(
|
||||
context.database_name, context.table_name
|
||||
)
|
||||
return {
|
||||
col_name: {
|
||||
"type": ct.name,
|
||||
"config": ct.config,
|
||||
}
|
||||
for col_name, ct in ct_map.items()
|
||||
}
|
||||
|
||||
|
||||
class SetColumnTypeUiExtra(Extra):
|
||||
description = "Column type UI metadata for this table"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
if context.is_view:
|
||||
return None
|
||||
|
||||
if not await context.datasette.allowed(
|
||||
action="set-column-type",
|
||||
resource=TableResource(
|
||||
database=context.database_name, table=context.table_name
|
||||
),
|
||||
actor=context.request.actor,
|
||||
):
|
||||
return None
|
||||
|
||||
column_details = await context.datasette._get_resource_column_details(
|
||||
context.database_name, context.table_name
|
||||
)
|
||||
ct_map = await context.datasette.get_column_types(
|
||||
context.database_name, context.table_name
|
||||
)
|
||||
columns = {}
|
||||
for column_name, column_detail in column_details.items():
|
||||
current = ct_map.get(column_name)
|
||||
columns[column_name] = {
|
||||
"current": (
|
||||
{"type": current.name, "config": current.config}
|
||||
if current is not None
|
||||
else None
|
||||
),
|
||||
"options": [
|
||||
{
|
||||
"name": name,
|
||||
"description": ct_cls.description,
|
||||
}
|
||||
for name, ct_cls in sorted(context.datasette._column_types.items())
|
||||
if context.datasette._column_type_is_applicable(
|
||||
ct_cls, column_detail
|
||||
)
|
||||
],
|
||||
}
|
||||
return {
|
||||
"path": "{}/-/set-column-type".format(
|
||||
context.datasette.urls.table(context.database_name, context.table_name)
|
||||
),
|
||||
"columns": columns,
|
||||
}
|
||||
|
||||
|
||||
class MetadataExtra(Extra):
|
||||
description = "Metadata about the table and database"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
tablemetadata = await context.datasette.get_resource_metadata(
|
||||
context.database_name, context.table_name
|
||||
)
|
||||
|
||||
rows = await context.datasette.get_internal_database().execute(
|
||||
"""
|
||||
SELECT
|
||||
column_name,
|
||||
value
|
||||
FROM metadata_columns
|
||||
WHERE database_name = ?
|
||||
AND resource_name = ?
|
||||
AND key = 'description'
|
||||
""",
|
||||
[context.database_name, context.table_name],
|
||||
)
|
||||
tablemetadata["columns"] = dict(rows)
|
||||
return tablemetadata
|
||||
|
||||
|
||||
class DatabaseExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.database_name
|
||||
|
||||
|
||||
class TableExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.table_name
|
||||
|
||||
|
||||
class DatabaseColorExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.db.color
|
||||
|
||||
|
||||
class FormHiddenArgsExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
form_hidden_args = []
|
||||
for key in context.request.args:
|
||||
if (
|
||||
key.startswith("_")
|
||||
and key not in ("_sort", "_sort_desc", "_search", "_next")
|
||||
and "__" not in key
|
||||
):
|
||||
for value in context.request.args.getlist(key):
|
||||
form_hidden_args.append((key, value))
|
||||
return form_hidden_args
|
||||
|
||||
|
||||
class FiltersExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.filters
|
||||
|
||||
|
||||
class CustomTableTemplatesExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return [
|
||||
f"_table-{to_css_class(context.database_name)}-{to_css_class(context.table_name)}.html",
|
||||
f"_table-table-{to_css_class(context.database_name)}-{to_css_class(context.table_name)}.html",
|
||||
"_table.html",
|
||||
]
|
||||
|
||||
|
||||
class SortedFacetResultsExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context, facet_results):
|
||||
facet_configs = context.table_metadata.get("facets", [])
|
||||
if facet_configs:
|
||||
metadata_facet_names = []
|
||||
for fc in facet_configs:
|
||||
if isinstance(fc, str):
|
||||
metadata_facet_names.append(fc)
|
||||
elif isinstance(fc, dict):
|
||||
metadata_facet_names.append(list(fc.values())[0])
|
||||
metadata_order = {name: i for i, name in enumerate(metadata_facet_names)}
|
||||
metadata_facets = []
|
||||
request_facets = []
|
||||
for f in facet_results["results"].values():
|
||||
if f["name"] in metadata_order:
|
||||
metadata_facets.append(f)
|
||||
else:
|
||||
request_facets.append(f)
|
||||
metadata_facets.sort(key=lambda f: metadata_order[f["name"]])
|
||||
request_facets.sort(
|
||||
key=lambda f: (len(f["results"]), f["name"]),
|
||||
reverse=True,
|
||||
)
|
||||
return metadata_facets + request_facets
|
||||
else:
|
||||
return sorted(
|
||||
facet_results["results"].values(),
|
||||
key=lambda f: (len(f["results"]), f["name"]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
class TableDefinitionExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return await context.db.get_table_definition(context.table_name)
|
||||
|
||||
|
||||
class ViewDefinitionExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return await context.db.get_view_definition(context.table_name)
|
||||
|
||||
|
||||
class RenderersExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context, expandable_columns, query):
|
||||
renderers = {}
|
||||
url_labels_extra = {}
|
||||
if expandable_columns:
|
||||
url_labels_extra = {"_labels": "on"}
|
||||
for key, (_, can_render) in context.datasette.renderers.items():
|
||||
it_can_render = call_with_supported_arguments(
|
||||
can_render,
|
||||
datasette=context.datasette,
|
||||
columns=context.columns or [],
|
||||
rows=context.rows or [],
|
||||
sql=query.get("sql", None),
|
||||
query_name=None,
|
||||
database=context.database_name,
|
||||
table=context.table_name,
|
||||
request=context.request,
|
||||
view_name="table",
|
||||
)
|
||||
it_can_render = await await_me_maybe(it_can_render)
|
||||
if it_can_render:
|
||||
renderers[key] = context.datasette.urls.path(
|
||||
path_with_format(
|
||||
request=context.request,
|
||||
path=context.request.scope.get("route_path"),
|
||||
format=key,
|
||||
extra_qs={**url_labels_extra},
|
||||
)
|
||||
)
|
||||
return renderers
|
||||
|
||||
|
||||
class PrivateExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
return context.private
|
||||
|
||||
|
||||
class ExpandableColumnsExtra(Extra):
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
expandables = []
|
||||
db = context.datasette.databases[context.database_name]
|
||||
for fk in await db.foreign_keys_for_table(context.table_name):
|
||||
label_column = await db.label_column_for_table(fk["other_table"])
|
||||
expandables.append((fk, label_column))
|
||||
return expandables
|
||||
|
||||
|
||||
class ExtrasExtra(Extra):
|
||||
description = "Available ?_extra= blocks"
|
||||
scopes = frozenset({ExtraScope.TABLE})
|
||||
|
||||
async def resolve(self, context):
|
||||
all_extras = [
|
||||
(cls.key(), cls.description)
|
||||
for cls in context.extra_registry.public_classes_for_scope(ExtraScope.TABLE)
|
||||
]
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
"description": description,
|
||||
"toggle_url": context.datasette.absolute_url(
|
||||
context.request,
|
||||
context.datasette.urls.path(
|
||||
path_with_added_args(context.request, {"_extra": name})
|
||||
if name not in context.extras
|
||||
else path_with_removed_args(context.request, {"_extra": name})
|
||||
),
|
||||
),
|
||||
"selected": name in context.extras,
|
||||
}
|
||||
for name, description in all_extras
|
||||
]
|
||||
|
||||
|
||||
TABLE_EXTRA_BUNDLES = {
|
||||
"html": [
|
||||
"suggested_facets",
|
||||
"facet_results",
|
||||
"facets_timed_out",
|
||||
"count",
|
||||
"count_sql",
|
||||
"human_description_en",
|
||||
"next_url",
|
||||
"metadata",
|
||||
"query",
|
||||
"columns",
|
||||
"display_columns",
|
||||
"display_rows",
|
||||
"database",
|
||||
"table",
|
||||
"database_color",
|
||||
"actions",
|
||||
"filters",
|
||||
"renderers",
|
||||
"custom_table_templates",
|
||||
"sorted_facet_results",
|
||||
"table_definition",
|
||||
"view_definition",
|
||||
"is_view",
|
||||
"private",
|
||||
"primary_keys",
|
||||
"all_columns",
|
||||
"expandable_columns",
|
||||
"form_hidden_args",
|
||||
"set_column_type_ui",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
TABLE_EXTRA_CLASSES = [
|
||||
CountExtra,
|
||||
CountSqlExtra,
|
||||
FacetResultsExtra,
|
||||
FacetsTimedOutExtra,
|
||||
SuggestedFacetsExtra,
|
||||
FacetInstancesProvider,
|
||||
HumanDescriptionEnExtra,
|
||||
NextUrlExtra,
|
||||
ColumnsExtra,
|
||||
AllColumnsExtra,
|
||||
PrimaryKeysExtra,
|
||||
DisplayColumnsAndRowsProvider,
|
||||
DisplayColumnsExtra,
|
||||
DisplayRowsExtra,
|
||||
RenderCellExtra,
|
||||
DebugExtra,
|
||||
RequestExtra,
|
||||
QueryExtra,
|
||||
ColumnTypesExtra,
|
||||
SetColumnTypeUiExtra,
|
||||
MetadataExtra,
|
||||
ExtrasExtra,
|
||||
DatabaseExtra,
|
||||
TableExtra,
|
||||
DatabaseColorExtra,
|
||||
ActionsExtra,
|
||||
FiltersExtra,
|
||||
RenderersExtra,
|
||||
CustomTableTemplatesExtra,
|
||||
SortedFacetResultsExtra,
|
||||
TableDefinitionExtra,
|
||||
ViewDefinitionExtra,
|
||||
IsViewExtra,
|
||||
PrivateExtra,
|
||||
ExpandableColumnsExtra,
|
||||
FormHiddenArgsExtra,
|
||||
]
|
||||
|
||||
|
||||
table_extra_registry = ExtraRegistry(TABLE_EXTRA_CLASSES)
|
||||
|
||||
|
||||
async def resolve_table_extras(extras, context):
|
||||
return await table_extra_registry.resolve(extras, context, ExtraScope.TABLE)
|
||||
Loading…
Add table
Add a link
Reference in a new issue