mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Magic parameters for canned queries
Closes #842 Includes a new plugin hook, register_magic_parameters()
This commit is contained in:
parent
4b142862f2
commit
563f5a2d3a
14 changed files with 477 additions and 167 deletions
|
|
@ -132,7 +132,7 @@ class Database:
|
||||||
with sqlite_timelimit(conn, time_limit_ms):
|
with sqlite_timelimit(conn, time_limit_ms):
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(sql, params or {})
|
cursor.execute(sql, params if params is not None else {})
|
||||||
max_returned_rows = self.ds.max_returned_rows
|
max_returned_rows = self.ds.max_returned_rows
|
||||||
if max_returned_rows == page_size:
|
if max_returned_rows == page_size:
|
||||||
max_returned_rows += 1
|
max_returned_rows += 1
|
||||||
|
|
|
||||||
55
datasette/default_magic_parameters.py
Normal file
55
datasette/default_magic_parameters.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
from datasette import hookimpl
|
||||||
|
from datasette.utils import escape_fts
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def header(key, request):
|
||||||
|
key = key.replace("_", "-").encode("utf-8")
|
||||||
|
headers_dict = dict(request.scope["headers"])
|
||||||
|
return headers_dict[key].decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def actor(key, request):
|
||||||
|
if request.actor is None:
|
||||||
|
raise KeyError
|
||||||
|
return request.actor[key]
|
||||||
|
|
||||||
|
|
||||||
|
def cookie(key, request):
|
||||||
|
return request.cookies[key]
|
||||||
|
|
||||||
|
|
||||||
|
def timestamp(key, request):
|
||||||
|
if key == "epoch":
|
||||||
|
return int(time.time())
|
||||||
|
elif key == "date_utc":
|
||||||
|
return datetime.datetime.utcnow().date().isoformat()
|
||||||
|
elif key == "datetime_utc":
|
||||||
|
return datetime.datetime.utcnow().strftime(r"%Y-%m-%dT%H:%M:%S") + "Z"
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
|
||||||
|
def random(key, request):
|
||||||
|
if key.startswith("chars_") and key.split("chars_")[-1].isdigit():
|
||||||
|
num_chars = int(key.split("chars_")[-1])
|
||||||
|
if num_chars % 2 == 1:
|
||||||
|
urandom_len = (num_chars + 1) / 2
|
||||||
|
else:
|
||||||
|
urandom_len = num_chars / 2
|
||||||
|
return os.urandom(int(urandom_len)).hex()[:num_chars]
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_magic_parameters():
|
||||||
|
return [
|
||||||
|
("header", header),
|
||||||
|
("actor", actor),
|
||||||
|
("cookie", cookie),
|
||||||
|
("timestamp", timestamp),
|
||||||
|
("random", random),
|
||||||
|
]
|
||||||
|
|
@ -83,3 +83,8 @@ def permission_allowed(datasette, actor, action, resource):
|
||||||
@hookspec
|
@hookspec
|
||||||
def canned_queries(datasette, database, actor):
|
def canned_queries(datasette, database, actor):
|
||||||
"Return a dictonary of canned query definitions or an awaitable function that returns them"
|
"Return a dictonary of canned query definitions or an awaitable function that returns them"
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def register_magic_parameters(datasette):
|
||||||
|
"Return a list of (name, function) magic parameter functions"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ DEFAULT_PLUGINS = (
|
||||||
"datasette.sql_functions",
|
"datasette.sql_functions",
|
||||||
"datasette.actor_auth_cookie",
|
"datasette.actor_auth_cookie",
|
||||||
"datasette.default_permissions",
|
"datasette.default_permissions",
|
||||||
|
"datasette.default_magic_parameters",
|
||||||
)
|
)
|
||||||
|
|
||||||
pm = pluggy.PluginManager("datasette")
|
pm = pluggy.PluginManager("datasette")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import base64
|
||||||
import click
|
import click
|
||||||
import hashlib
|
import hashlib
|
||||||
import inspect
|
import inspect
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import mergedeep
|
import mergedeep
|
||||||
import os
|
import os
|
||||||
|
|
@ -17,6 +18,7 @@ import urllib
|
||||||
import numbers
|
import numbers
|
||||||
import yaml
|
import yaml
|
||||||
from .shutil_backport import copytree
|
from .shutil_backport import copytree
|
||||||
|
from ..plugins import pm
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pysqlite3 as sqlite3
|
import pysqlite3 as sqlite3
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import itertools
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
from datasette.utils import (
|
from datasette.utils import (
|
||||||
|
|
@ -165,11 +166,12 @@ class QueryView(DataView):
|
||||||
named_parameter_values = {
|
named_parameter_values = {
|
||||||
named_parameter: params.get(named_parameter) or ""
|
named_parameter: params.get(named_parameter) or ""
|
||||||
for named_parameter in named_parameters
|
for named_parameter in named_parameters
|
||||||
|
if not named_parameter.startswith("_")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set to blank string if missing from params
|
# Set to blank string if missing from params
|
||||||
for named_parameter in named_parameters:
|
for named_parameter in named_parameters:
|
||||||
if named_parameter not in params:
|
if named_parameter not in params and not named_parameter.startswith("_"):
|
||||||
params[named_parameter] = ""
|
params[named_parameter] = ""
|
||||||
|
|
||||||
extra_args = {}
|
extra_args = {}
|
||||||
|
|
@ -184,9 +186,13 @@ class QueryView(DataView):
|
||||||
if write:
|
if write:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
params = await request.post_vars()
|
params = await request.post_vars()
|
||||||
|
if canned_query:
|
||||||
|
params_for_query = MagicParameters(params, request, self.ds)
|
||||||
|
else:
|
||||||
|
params_for_query = params
|
||||||
try:
|
try:
|
||||||
cursor = await self.ds.databases[database].execute_write(
|
cursor = await self.ds.databases[database].execute_write(
|
||||||
sql, params, block=True
|
sql, params_for_query, block=True
|
||||||
)
|
)
|
||||||
message = metadata.get(
|
message = metadata.get(
|
||||||
"on_success_message"
|
"on_success_message"
|
||||||
|
|
@ -227,8 +233,12 @@ class QueryView(DataView):
|
||||||
templates,
|
templates,
|
||||||
)
|
)
|
||||||
else: # Not a write
|
else: # Not a write
|
||||||
|
if canned_query:
|
||||||
|
params_for_query = MagicParameters(params, request, self.ds)
|
||||||
|
else:
|
||||||
|
params_for_query = params
|
||||||
results = await self.ds.execute(
|
results = await self.ds.execute(
|
||||||
database, sql, params, truncate=True, **extra_args
|
database, sql, params_for_query, truncate=True, **extra_args
|
||||||
)
|
)
|
||||||
columns = [r[0] for r in results.description]
|
columns = [r[0] for r in results.description]
|
||||||
|
|
||||||
|
|
@ -298,3 +308,25 @@ class QueryView(DataView):
|
||||||
extra_template,
|
extra_template,
|
||||||
templates,
|
templates,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MagicParameters(dict):
|
||||||
|
def __init__(self, data, request, datasette):
|
||||||
|
super().__init__(data)
|
||||||
|
self._request = request
|
||||||
|
self._magics = dict(
|
||||||
|
itertools.chain.from_iterable(
|
||||||
|
pm.hook.register_magic_parameters(datasette=datasette)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key.startswith("_") and key.count("_") >= 2:
|
||||||
|
prefix, suffix = key[1:].split("_", 1)
|
||||||
|
if prefix in self._magics:
|
||||||
|
try:
|
||||||
|
return self._magics[prefix](suffix, self._request)
|
||||||
|
except KeyError:
|
||||||
|
return super().__getitem__(key)
|
||||||
|
else:
|
||||||
|
return super().__getitem__(key)
|
||||||
|
|
|
||||||
|
|
@ -895,3 +895,42 @@ Here's an example that allows users to view the ``admin_log`` table only if thei
|
||||||
See :ref:`built-in permissions <permissions>` for a full list of permissions that are included in Datasette core.
|
See :ref:`built-in permissions <permissions>` for a full list of permissions that are included in Datasette core.
|
||||||
|
|
||||||
Example: `datasette-permissions-sql <https://github.com/simonw/datasette-permissions-sql>`_
|
Example: `datasette-permissions-sql <https://github.com/simonw/datasette-permissions-sql>`_
|
||||||
|
|
||||||
|
.. _plugin_hook_register_magic_parameters:
|
||||||
|
|
||||||
|
register_magic_parameters(datasette)
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
``datasette`` - :ref:`internals_datasette`
|
||||||
|
You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``.
|
||||||
|
|
||||||
|
:ref:`canned_queries_magic_parameters` can be used to add automatic parameters to :ref:`canned queries <canned_queries>`. This plugin hook allows additional magic parameters to be defined by plugins.
|
||||||
|
|
||||||
|
Magic parameters all take this format: ``_prefix_rest_of_parameter``. The prefix indicates which magic parameter function should be called - the rest of the parameter is passed as an argument to that function.
|
||||||
|
|
||||||
|
To register a new function, return it as a tuple of ``(string prefix, function)`` from this hook. The function you register should take two arguments: ``key`` and ``request``, where ``key`` is the ``rest_of_parameter`` portion of the parameter and ``request`` is the current :ref:`internals_request`.
|
||||||
|
|
||||||
|
This example registers two new magic parameters: ``:_request_http_version`` returning the HTTP version of the current request, and ``:_uuid_new`` which returns a new UUID:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
def uuid(key, request):
|
||||||
|
if key == "new":
|
||||||
|
return str(uuid4())
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
def request(key, request):
|
||||||
|
if key == "http_version":
|
||||||
|
return request.scope["http_version"]
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_magic_parameters(datasette):
|
||||||
|
return [
|
||||||
|
("request", request),
|
||||||
|
("uuid", uuid),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -114,8 +114,8 @@ rendered as HTML (rather than having HTML special characters escaped).
|
||||||
|
|
||||||
.. _canned_queries_named_parameters:
|
.. _canned_queries_named_parameters:
|
||||||
|
|
||||||
Named parameters
|
Canned query parameters
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Canned queries support named parameters, so if you include those in the SQL you
|
Canned queries support named parameters, so if you include those in the SQL you
|
||||||
will then be able to enter them using the form fields on the canned query page
|
will then be able to enter them using the form fields on the canned query page
|
||||||
|
|
@ -274,6 +274,58 @@ You can use ``"params"`` to explicitly list the named parameters that should be
|
||||||
|
|
||||||
You can pre-populate form fields when the page first loads using a querystring, e.g. ``/mydatabase/add_name?name=Prepopulated``. The user will have to submit the form to execute the query.
|
You can pre-populate form fields when the page first loads using a querystring, e.g. ``/mydatabase/add_name?name=Prepopulated``. The user will have to submit the form to execute the query.
|
||||||
|
|
||||||
|
.. _canned_queries_magic_parameters:
|
||||||
|
|
||||||
|
Magic parameters
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Named parameters that start with an underscore are special: they can be used to automatically add values created by Datasette that are not contained in the incoming form fields or querystring.
|
||||||
|
|
||||||
|
Available magic parameters are:
|
||||||
|
|
||||||
|
``_actor_*`` - e.g. ``_actor_id``, ``_actor_name``
|
||||||
|
Fields from the currently authenticated :ref:`authentication_actor`.
|
||||||
|
|
||||||
|
``_header_*`` - e.g. ``_header_user_agent``
|
||||||
|
Header from the incoming HTTP request. The key should be in lower case and with hyphens converted to underscores e.g. ``_header_user_agent`` or ``_header_accept_language``.
|
||||||
|
|
||||||
|
``_cookie_*`` - e.g. ``_cookie_lang``
|
||||||
|
The value of the incoming cookie of that name.
|
||||||
|
|
||||||
|
``_timestamp_epoch``
|
||||||
|
The number of seconds since the Unix epoch.
|
||||||
|
|
||||||
|
``_timestamp_date_utc``
|
||||||
|
The date in UTC, e.g. ``2020-06-01``
|
||||||
|
|
||||||
|
``_timestamp_datetime_utc``
|
||||||
|
The ISO 8601 datetime in UTC, e.g. ``2020-06-24T18:01:07Z``
|
||||||
|
|
||||||
|
``_random_chars_*`` - e.g. ``_random_chars_128``
|
||||||
|
A random string of characters of the specified length.
|
||||||
|
|
||||||
|
Here's an example configuration (this time using ``metadata.yaml`` since that provides better support for multi-line SQL queries) that adds a message from the authenticated user, storing various pieces of additional metadata using magic parameters:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
databases:
|
||||||
|
mydatabase:
|
||||||
|
queries:
|
||||||
|
add_message:
|
||||||
|
allow:
|
||||||
|
id: "*"
|
||||||
|
sql: |-
|
||||||
|
INSERT INTO messages (
|
||||||
|
user_id, ip, message, datetime
|
||||||
|
) VALUES (
|
||||||
|
:_actor_id, :_request_ip, :message, :_timestamp_datetime_utc
|
||||||
|
)
|
||||||
|
write: true
|
||||||
|
|
||||||
|
The form presented at ``/mydatabase/add_message`` will have just a field for ``message`` - the other parameters will be populated by the magic parameter mechanism.
|
||||||
|
|
||||||
|
Additional custom magic parameters can be added by plugins using the :ref:`plugin_hook_register_magic_parameters` hook.
|
||||||
|
|
||||||
.. _pagination:
|
.. _pagination:
|
||||||
|
|
||||||
Pagination
|
Pagination
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ EXPECTED_PLUGINS = [
|
||||||
"prepare_connection",
|
"prepare_connection",
|
||||||
"prepare_jinja2_environment",
|
"prepare_jinja2_environment",
|
||||||
"register_facet_classes",
|
"register_facet_classes",
|
||||||
|
"register_magic_parameters",
|
||||||
"register_routes",
|
"register_routes",
|
||||||
"render_cell",
|
"render_cell",
|
||||||
"startup",
|
"startup",
|
||||||
|
|
|
||||||
|
|
@ -212,3 +212,25 @@ def canned_queries(datasette, database, actor):
|
||||||
actor["id"] if actor else "null"
|
actor["id"] if actor else "null"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def register_magic_parameters():
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
def uuid(key, request):
|
||||||
|
if key == "new":
|
||||||
|
return str(uuid4())
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
def request(key, request):
|
||||||
|
if key == "http_version":
|
||||||
|
return request.scope["http_version"]
|
||||||
|
else:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
return [
|
||||||
|
("request", request),
|
||||||
|
("uuid", uuid),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -601,18 +601,6 @@ def test_custom_sql(app_client):
|
||||||
assert not data["truncated"]
|
assert not data["truncated"]
|
||||||
|
|
||||||
|
|
||||||
def test_canned_query_with_named_parameter(app_client):
|
|
||||||
response = app_client.get("/fixtures/neighborhood_search.json?text=town")
|
|
||||||
assert [
|
|
||||||
["Corktown", "Detroit", "MI"],
|
|
||||||
["Downtown", "Los Angeles", "CA"],
|
|
||||||
["Downtown", "Detroit", "MI"],
|
|
||||||
["Greektown", "Detroit", "MI"],
|
|
||||||
["Koreatown", "Los Angeles", "CA"],
|
|
||||||
["Mexicantown", "Detroit", "MI"],
|
|
||||||
] == response.json["rows"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_sql_time_limit(app_client_shorter_time_limit):
|
def test_sql_time_limit(app_client_shorter_time_limit):
|
||||||
response = app_client_shorter_time_limit.get("/fixtures.json?sql=select+sleep(0.5)")
|
response = app_client_shorter_time_limit.get("/fixtures.json?sql=select+sleep(0.5)")
|
||||||
assert 400 == response.status
|
assert 400 == response.status
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
from bs4 import BeautifulSoup as Soup
|
||||||
import pytest
|
import pytest
|
||||||
from .fixtures import make_app_client
|
import re
|
||||||
|
from .fixtures import make_app_client, app_client
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -39,6 +41,18 @@ def canned_write_client():
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
def test_canned_query_with_named_parameter(app_client):
|
||||||
|
response = app_client.get("/fixtures/neighborhood_search.json?text=town")
|
||||||
|
assert [
|
||||||
|
["Corktown", "Detroit", "MI"],
|
||||||
|
["Downtown", "Los Angeles", "CA"],
|
||||||
|
["Downtown", "Detroit", "MI"],
|
||||||
|
["Greektown", "Detroit", "MI"],
|
||||||
|
["Koreatown", "Los Angeles", "CA"],
|
||||||
|
["Mexicantown", "Detroit", "MI"],
|
||||||
|
] == response.json["rows"]
|
||||||
|
|
||||||
|
|
||||||
def test_insert(canned_write_client):
|
def test_insert(canned_write_client):
|
||||||
response = canned_write_client.post(
|
response = canned_write_client.post(
|
||||||
"/data/add_name", {"name": "Hello"}, allow_redirects=False, csrftoken_from=True,
|
"/data/add_name", {"name": "Hello"}, allow_redirects=False, csrftoken_from=True,
|
||||||
|
|
@ -147,3 +161,74 @@ def test_canned_query_permissions(canned_write_client):
|
||||||
cookies = {"ds_actor": canned_write_client.actor_cookie({"id": "root"})}
|
cookies = {"ds_actor": canned_write_client.actor_cookie({"id": "root"})}
|
||||||
assert 200 == canned_write_client.get("/data/delete_name", cookies=cookies).status
|
assert 200 == canned_write_client.get("/data/delete_name", cookies=cookies).status
|
||||||
assert 200 == canned_write_client.get("/data/update_name", cookies=cookies).status
|
assert 200 == canned_write_client.get("/data/update_name", cookies=cookies).status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def magic_parameters_client():
|
||||||
|
with make_app_client(
|
||||||
|
extra_databases={"data.db": "create table logs (line text)"},
|
||||||
|
metadata={
|
||||||
|
"databases": {
|
||||||
|
"data": {
|
||||||
|
"queries": {
|
||||||
|
"runme_post": {"sql": "", "write": True},
|
||||||
|
"runme_get": {"sql": ""},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"magic_parameter,expected_re",
|
||||||
|
[
|
||||||
|
("_actor_id", "root"),
|
||||||
|
("_header_host", "localhost"),
|
||||||
|
("_cookie_foo", "bar"),
|
||||||
|
("_timestamp_epoch", r"^\d+$"),
|
||||||
|
("_timestamp_date_utc", r"^\d{4}-\d{2}-\d{2}$"),
|
||||||
|
("_timestamp_datetime_utc", r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"),
|
||||||
|
("_random_chars_1", r"^\w$"),
|
||||||
|
("_random_chars_10", r"^\w{10}$"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_magic_parameters(magic_parameters_client, magic_parameter, expected_re):
|
||||||
|
magic_parameters_client.ds._metadata["databases"]["data"]["queries"]["runme_post"][
|
||||||
|
"sql"
|
||||||
|
] = "insert into logs (line) values (:{})".format(magic_parameter)
|
||||||
|
magic_parameters_client.ds._metadata["databases"]["data"]["queries"]["runme_get"][
|
||||||
|
"sql"
|
||||||
|
] = "select :{} as result".format(magic_parameter)
|
||||||
|
cookies = {
|
||||||
|
"ds_actor": magic_parameters_client.actor_cookie({"id": "root"}),
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
# Test the GET version
|
||||||
|
get_response = magic_parameters_client.get(
|
||||||
|
"/data/runme_get.json?_shape=array", cookies=cookies
|
||||||
|
)
|
||||||
|
get_actual = get_response.json[0]["result"]
|
||||||
|
assert re.match(expected_re, str(get_actual))
|
||||||
|
# Test the form
|
||||||
|
form_response = magic_parameters_client.get("/data/runme_post")
|
||||||
|
soup = Soup(form_response.body, "html.parser")
|
||||||
|
# The magic parameter should not be represented as a form field
|
||||||
|
assert None is soup.find("input", {"name": magic_parameter})
|
||||||
|
# Submit the form to create a log line
|
||||||
|
response = magic_parameters_client.post(
|
||||||
|
"/data/runme_post", {}, csrftoken_from=True, cookies=cookies
|
||||||
|
)
|
||||||
|
post_actual = magic_parameters_client.get(
|
||||||
|
"/data/logs.json?_sort_desc=rowid&_shape=array"
|
||||||
|
).json[0]["line"]
|
||||||
|
assert re.match(expected_re, post_actual)
|
||||||
|
|
||||||
|
|
||||||
|
def test_magic_parameters_cannot_be_used_in_arbitrary_queries(magic_parameters_client):
|
||||||
|
response = magic_parameters_client.get(
|
||||||
|
"/data.json?sql=select+:_header_host&_shape=array"
|
||||||
|
)
|
||||||
|
assert 500 == response.status
|
||||||
|
assert "You did not supply a value for binding 1." == response.json["error"]
|
||||||
|
|
@ -638,3 +638,31 @@ def test_canned_queries_actor(app_client):
|
||||||
assert [{"1": 1, "actor_id": "bot"}] == app_client.get(
|
assert [{"1": 1, "actor_id": "bot"}] == app_client.get(
|
||||||
"/fixtures/from_hook.json?_bot=1&_shape=array"
|
"/fixtures/from_hook.json?_bot=1&_shape=array"
|
||||||
).json
|
).json
|
||||||
|
|
||||||
|
|
||||||
|
def test_register_magic_parameters(restore_working_directory):
|
||||||
|
with make_app_client(
|
||||||
|
extra_databases={"data.db": "create table logs (line text)"},
|
||||||
|
metadata={
|
||||||
|
"databases": {
|
||||||
|
"data": {
|
||||||
|
"queries": {
|
||||||
|
"runme": {
|
||||||
|
"sql": "insert into logs (line) values (:_request_http_version)",
|
||||||
|
"write": True,
|
||||||
|
},
|
||||||
|
"get_uuid": {"sql": "select :_uuid_new",},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) as client:
|
||||||
|
response = client.post("/data/runme", {}, csrftoken_from=True)
|
||||||
|
assert 200 == response.status
|
||||||
|
actual = client.get("/data/logs.json?_sort_desc=rowid&_shape=array").json
|
||||||
|
assert [{"rowid": 1, "line": "1.0"}] == actual
|
||||||
|
# Now try the GET request against get_uuid
|
||||||
|
response_get = client.get("/data/get_uuid.json?_shape=array")
|
||||||
|
assert 200 == response_get.status
|
||||||
|
new_uuid = response_get.json[0][":_uuid_new"]
|
||||||
|
assert 4 == new_uuid.count("-")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue