Precompute action permissions for table pages

- Extract reusable helpers for database and table action permission preloading.
- Precompute those permissions before building table-page HTML data.
- Document the default table actions plugin.
This commit is contained in:
Simon Willison 2026-06-17 10:22:42 -07:00
commit 4115213e17
4 changed files with 65 additions and 25 deletions

View file

@ -54,6 +54,8 @@ from .database import QueryView, _custom_column_type_options_for_create_table
from .table_extras import (
TABLE_EXTRA_BUNDLES,
TableExtraContext,
precompute_database_action_permissions,
precompute_table_action_permissions,
resolve_table_extras,
table_extra_registry,
)
@ -1247,7 +1249,10 @@ class TableAlterView(BaseView):
defaults[args.name] = _literal_default(
db_for_write, args.default
)
if "default_expr" in args.model_fields_set and not args.not_null:
if (
"default_expr" in args.model_fields_set
and not args.not_null
):
defaults[args.name] = _default_expression_sql(
args.default_expr
)
@ -1363,9 +1368,9 @@ class TableAlterView(BaseView):
"altered": altered,
"schema": after_schema,
"before_schema": before_schema,
"operations_applied": 0
if alter_request.dry_run
else len(alter_request.operations),
"operations_applied": (
0 if alter_request.dry_run else len(alter_request.operations)
),
"dry_run": alter_request.dry_run,
},
status=200,
@ -2068,6 +2073,15 @@ async def table_view_data(
if redirect_response:
return redirect_response
if context_for_html_hack:
await precompute_database_action_permissions(
datasette, request.actor, database_name
)
if not is_view:
await precompute_table_action_permissions(
datasette, request.actor, database_name, table_name
)
# Introspect columns and primary keys for table
pks = await db.primary_keys(table_name)
table_columns = await db.table_columns(table_name)

View file

@ -367,23 +367,14 @@ class ActionsExtra(Extra):
# that allowed() calls made inside the plugin hooks below
# are served from the cache
datasette = context.datasette
await datasette.allowed_many(
actions=[
name
for name, action in datasette.actions.items()
if action.resource_class is TableResource
],
resource=TableResource(context.database_name, context.table_name),
actor=context.request.actor,
await precompute_table_action_permissions(
datasette,
context.request.actor,
context.database_name,
context.table_name,
)
await datasette.allowed_many(
actions=[
name
for name, action in datasette.actions.items()
if action.resource_class is DatabaseResource
],
resource=DatabaseResource(context.database_name),
actor=context.request.actor,
await precompute_database_action_permissions(
datasette, context.request.actor, context.database_name
)
for hook in method(**kwargs):
extra_links = await await_me_maybe(hook)
@ -394,6 +385,32 @@ class ActionsExtra(Extra):
return actions
async def precompute_table_action_permissions(
datasette, actor, database_name, table_name
):
await datasette.allowed_many(
actions=[
name
for name, action in datasette.actions.items()
if action.resource_class is TableResource
],
resource=TableResource(database_name, table_name),
actor=actor,
)
async def precompute_database_action_permissions(datasette, actor, database_name):
await datasette.allowed_many(
actions=[
name
for name, action in datasette.actions.items()
if action.resource_class is DatabaseResource
],
resource=DatabaseResource(database_name),
actor=actor,
)
class IsViewExtra(Extra):
description = "Whether this resource is a view instead of a table"
example = ExtraExample("/fixtures/simple_view.json?_extra=is_view")

View file

@ -280,6 +280,15 @@ If you run ``datasette plugins --all`` it will include default plugins that ship
"query_actions"
]
},
{
"name": "datasette.default_table_actions",
"static": false,
"templates": false,
"version": null,
"hooks": [
"table_actions"
]
},
{
"name": "datasette.events",
"static": false,

View file

@ -350,7 +350,9 @@ def test_alter_table_flow(page, datasette_server):
assert first_more_options.inner_text() == "> Advanced options"
first_more_options.click()
assert first_more_options.inner_text() == "v Hide options"
expanded_options_text = dialog.locator(".table-alter-column-details").first.inner_text()
expanded_options_text = dialog.locator(
".table-alter-column-details"
).first.inner_text()
assert dialog.locator(".table-alter-fields").evaluate(
"node => node.scrollWidth <= node.clientWidth + 1"
)
@ -500,8 +502,7 @@ def test_alter_table_cancel_skips_discard_prompt(page, datasette_server):
return dialog
page.goto(f"{datasette_server}data/projects")
page.evaluate(
"""
page.evaluate("""
() => {
window.__discardConfirmMessages = [];
window.confirm = (message) => {
@ -509,8 +510,7 @@ def test_alter_table_cancel_skips_discard_prompt(page, datasette_server):
return false;
};
}
"""
)
""")
dialog = open_alter_dialog()
dialog.locator(".table-alter-add-column").click()