Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions datasette/templates/_sql_parameter_scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,28 @@
};
}

function parameterName(control) {
return control.dataset.parameterName || control.name;
}

function syncParameterState(manager) {
manager.parameterState = new Map();
manager.section
.querySelectorAll("[data-parameter-control]")
.forEach((control) => {
manager.parameterState.set(control.name, controlState(control));
manager.parameterState.set(parameterName(control), controlState(control));
});
}

function createControl(parameter, id, state) {
function createControl(parameter, id, state, namePrefix) {
const control = document.createElement(state.expanded ? "textarea" : "input");
control.id = id;
control.name = parameter;
control.name = `${namePrefix || ""}${parameter}`;
control.value = state.value;
control.setAttribute("data-parameter-control", "");
if (namePrefix) {
control.dataset.parameterName = parameter;
}
if (state.expanded) {
control.rows = 5;
} else {
Expand All @@ -53,18 +60,24 @@
value,
selectionStart
) {
const replacement = createControl(control.name, control.id, {
value: value === undefined ? control.value : value,
expanded: expand,
});
const parameter = parameterName(control);
const replacement = createControl(
parameter,
control.id,
{
value: value === undefined ? control.value : value,
expanded: expand,
},
manager.namePrefix
);
button.textContent = expand ? "Collapse" : "Expand";
button.setAttribute("aria-expanded", expand ? "true" : "false");
control.replaceWith(replacement);
replacement.focus();
if (selectionStart !== undefined && replacement.setSelectionRange) {
replacement.setSelectionRange(selectionStart, selectionStart);
}
manager.parameterState.set(replacement.name, controlState(replacement));
manager.parameterState.set(parameter, controlState(replacement));
}

function renderParameters(manager, parameters) {
Expand Down Expand Up @@ -99,7 +112,7 @@
label.htmlFor = id;
label.textContent = parameter;

const control = createControl(parameter, id, state);
const control = createControl(parameter, id, state, manager.namePrefix);

row.append(label, control);
if (manager.allowExpand) {
Expand All @@ -124,7 +137,7 @@
if (!control.matches || !control.matches("[data-parameter-control]")) {
return;
}
manager.parameterState.set(control.name, controlState(control));
manager.parameterState.set(parameterName(control), controlState(control));
});

if (!manager.allowExpand) {
Expand Down Expand Up @@ -230,6 +243,7 @@
? section.dataset.allowExpand === "1"
: false
: options.allowExpand,
namePrefix: section ? section.dataset.parameterNamePrefix || "" : "",
parameterState: new Map(),
};
if (section) {
Expand Down
5 changes: 3 additions & 2 deletions datasette/templates/_sql_parameters.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<div id="{{ sql_parameters_section_id|default("sql-parameters-section") }}" class="sql-parameters-section" data-sql-parameters-section{% if sql_parameters_allow_expand|default(false) %} data-allow-expand="1"{% endif %}>
{% set sql_parameter_name_prefix = sql_parameter_name_prefix|default("") %}
<div id="{{ sql_parameters_section_id|default("sql-parameters-section") }}" class="sql-parameters-section" data-sql-parameters-section{% if sql_parameter_name_prefix %} data-parameter-name-prefix="{{ sql_parameter_name_prefix }}"{% endif %}{% if sql_parameters_allow_expand|default(false) %} data-allow-expand="1"{% endif %}>
{% if parameter_names %}
<h2>Parameters</h2>
{% for parameter in parameter_names %}
{% set parameter_id = (sql_parameter_id_prefix|default("qp")) ~ loop.index %}
<p class="sql-parameter-row"><label for="{{ parameter_id }}">{{ parameter }}</label> <input type="text" id="{{ parameter_id }}" name="{{ parameter }}" value="{{ parameter_values.get(parameter, "") }}" data-parameter-control>{% if sql_parameters_allow_expand|default(false) %} <button type="button" class="sql-parameter-toggle" data-parameter-toggle aria-controls="{{ parameter_id }}" aria-expanded="false">Expand</button>{% endif %}</p>
<p class="sql-parameter-row"><label for="{{ parameter_id }}">{{ parameter }}</label> <input type="text" id="{{ parameter_id }}" name="{{ sql_parameter_name_prefix }}{{ parameter }}" value="{{ parameter_values.get(parameter, "") }}" data-parameter-control{% if sql_parameter_name_prefix %} data-parameter-name="{{ parameter }}"{% endif %}>{% if sql_parameters_allow_expand|default(false) %} <button type="button" class="sql-parameter-toggle" data-parameter-toggle aria-controls="{{ parameter_id }}" aria-expanded="false">Expand</button>{% endif %}</p>
{% endfor %}
{% endif %}
</div>
2 changes: 2 additions & 0 deletions datasette/views/execute_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .database import display_rows as display_query_rows
from .query_helpers import (
QueryValidationError,
SQL_PARAMETER_FORM_PREFIX,
_analysis_is_write,
_analysis_rows,
_analysis_rows_with_permissions,
Expand Down Expand Up @@ -295,6 +296,7 @@ async def _render_form(
"execute_write_columns": execute_write_columns,
"execute_write_display_rows": execute_write_display_rows,
"execute_write_truncated": execute_write_truncated,
"sql_parameter_name_prefix": SQL_PARAMETER_FORM_PREFIX,
"execute_disabled": bool(execute_disabled_reason),
"execute_disabled_reason": execute_disabled_reason,
"table_columns": table_columns,
Expand Down
14 changes: 9 additions & 5 deletions datasette/views/query_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"on_error_redirect",
}

SQL_PARAMETER_FORM_PREFIX = "_sql_param_"


class QueryValidationError(Exception):
def __init__(self, message, status=400, *, flash=False):
Expand Down Expand Up @@ -289,11 +291,13 @@ def _coerce_execute_write_payload(data, is_json):
)
params = data.get("params") or {}
else:
params = {
key: value
for key, value in data.items()
if key not in {"sql", "csrftoken", "_json"}
}
params = {}
for key, value in data.items():
if key in {"sql", "csrftoken", "_json"}:
continue
if key.startswith(SQL_PARAMETER_FORM_PREFIX):
key = key[len(SQL_PARAMETER_FORM_PREFIX) :]
params[key] = value
if not isinstance(params, dict):
raise QueryValidationError("params must be a dictionary")
return data.get("sql"), params
Expand Down
38 changes: 38 additions & 0 deletions tests/test_api_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,44 @@ async def test_update_row_alter(ds_write):
assert response.json() == {"ok": True}


@pytest.mark.asyncio
async def test_execute_write_form_parameter_called_sql():
ds = Datasette(memory=True, default_deny=True)
ds.root_enabled = True
db = ds.add_memory_database("execute_write_parameter_sql", name="data")
await db.execute_write("create table docs (id integer primary key, title text)")
await db.execute_write("insert into docs (id, title) values (1, 'Initial')")
await ds.invoke_startup()

form_response = await ds.client.get(
"/data/-/execute-write",
actor={"id": "root"},
params={"sql": "update docs set title = :sql where id = :id"},
)
assert form_response.status_code == 200
assert 'data-parameter-name-prefix="_sql_param_"' in form_response.text
assert '<label for="qp1">sql</label>' in form_response.text
assert 'name="_sql_param_sql"' in form_response.text
assert 'data-parameter-name="sql"' in form_response.text
assert 'name="_sql_param_id"' in form_response.text

response = await ds.client.post(
"/data/-/execute-write",
actor={"id": "root"},
data={
"sql": "update docs set title = :sql where id = :id",
"_sql_param_sql": "Updated",
"_sql_param_id": "1",
},
)

assert response.status_code == 200
assert "Query executed, 1 row affected" in response.text
assert (await db.execute("select title from docs where id = 1")).first()[
0
] == "Updated"


@pytest.mark.asyncio
@pytest.mark.parametrize(
"input,expected_errors",
Expand Down