JSON API for writable canned queries, closes #880

pull/977/head
Simon Willison 2020-09-14 14:23:18 -07:00
rodzic 894999a14e
commit 72ac2fd32c
4 zmienionych plików z 110 dodań i 21 usunięć

Wyświetl plik

@ -55,6 +55,7 @@ class TestClient:
redirect_count=0, redirect_count=0,
content_type="application/x-www-form-urlencoded", content_type="application/x-www-form-urlencoded",
cookies=None, cookies=None,
headers=None,
csrftoken_from=None, csrftoken_from=None,
): ):
cookies = cookies or {} cookies = cookies or {}
@ -72,13 +73,14 @@ class TestClient:
if post_data: if post_data:
body = urlencode(post_data, doseq=True) body = urlencode(post_data, doseq=True)
return await self._request( return await self._request(
path, path=path,
allow_redirects, allow_redirects=allow_redirects,
redirect_count, redirect_count=redirect_count,
"POST", method="POST",
cookies, cookies=cookies,
body, headers=headers,
content_type, post_body=body,
content_type=content_type,
) )
async def _request( async def _request(
@ -88,6 +90,7 @@ class TestClient:
redirect_count=0, redirect_count=0,
method="GET", method="GET",
cookies=None, cookies=None,
headers=None,
post_body=None, post_body=None,
content_type=None, content_type=None,
): ):
@ -99,14 +102,17 @@ class TestClient:
raw_path = path.encode("latin-1") raw_path = path.encode("latin-1")
else: else:
raw_path = quote(path, safe="/:,").encode("latin-1") raw_path = quote(path, safe="/:,").encode("latin-1")
headers = [[b"host", b"localhost"]] asgi_headers = [[b"host", b"localhost"]]
if headers:
for key, value in headers.items():
asgi_headers.append([key.encode("utf-8"), value.encode("utf-8")])
if content_type: if content_type:
headers.append((b"content-type", content_type.encode("utf-8"))) asgi_headers.append((b"content-type", content_type.encode("utf-8")))
if cookies: if cookies:
sc = SimpleCookie() sc = SimpleCookie()
for key, value in cookies.items(): for key, value in cookies.items():
sc[key] = value sc[key] = value
headers.append([b"cookie", sc.output(header="").encode("utf-8")]) asgi_headers.append([b"cookie", sc.output(header="").encode("utf-8")])
scope = { scope = {
"type": "http", "type": "http",
"http_version": "1.0", "http_version": "1.0",
@ -114,7 +120,7 @@ class TestClient:
"path": unquote(path), "path": unquote(path),
"raw_path": raw_path, "raw_path": raw_path,
"query_string": query_string, "query_string": query_string,
"headers": headers, "headers": asgi_headers,
} }
instance = ApplicationCommunicator(self.asgi_app, scope) instance = ApplicationCommunicator(self.asgi_app, scope)

Wyświetl plik

@ -219,10 +219,17 @@ class QueryView(DataView):
params[key] = str(value) params[key] = str(value)
else: else:
params = dict(parse_qsl(body, keep_blank_values=True)) params = dict(parse_qsl(body, keep_blank_values=True))
# Should we return JSON?
should_return_json = (
request.headers.get("accept") == "application/json"
or request.args.get("_json")
or params.get("_json")
)
if canned_query: if canned_query:
params_for_query = MagicParameters(params, request, self.ds) params_for_query = MagicParameters(params, request, self.ds)
else: else:
params_for_query = params params_for_query = params
ok = None
try: try:
cursor = await self.ds.databases[database].execute_write( cursor = await self.ds.databases[database].execute_write(
sql, params_for_query, block=True sql, params_for_query, block=True
@ -234,12 +241,23 @@ class QueryView(DataView):
) )
message_type = self.ds.INFO message_type = self.ds.INFO
redirect_url = metadata.get("on_success_redirect") redirect_url = metadata.get("on_success_redirect")
ok = True
except Exception as e: except Exception as e:
message = metadata.get("on_error_message") or str(e) message = metadata.get("on_error_message") or str(e)
message_type = self.ds.ERROR message_type = self.ds.ERROR
redirect_url = metadata.get("on_error_redirect") redirect_url = metadata.get("on_error_redirect")
self.ds.add_message(request, message, message_type) ok = False
return self.redirect(request, redirect_url or request.path) if should_return_json:
return Response.json(
{
"ok": ok,
"message": message,
"redirect": redirect_url,
}
)
else:
self.ds.add_message(request, message, message_type)
return self.redirect(request, redirect_url or request.path)
else: else:
async def extra_template(): async def extra_template():

Wyświetl plik

@ -326,6 +326,43 @@ The form presented at ``/mydatabase/add_message`` will have just a field for ``m
Additional custom magic parameters can be added by plugins using the :ref:`plugin_hook_register_magic_parameters` hook. Additional custom magic parameters can be added by plugins using the :ref:`plugin_hook_register_magic_parameters` hook.
.. _canned_queries_json_api:
JSON API for writable canned queries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Writable canned queries can also be accessed using a JSON API. You can POST data to them using JSON, and you can request that their response is returned to you as JSON.
To submit JSON to a writable canned query, encode key/value parameters as a JSON document::
POST /mydatabase/add_message
{"message": "Message goes here"}
You can also continue to submit data using regular form encoding, like so::
POST /mydatabase/add_message
message=Message+goes+here
There are three options for specifying that you would like the response to your request to return JSON data, as opposed to an HTTP redirect to another page.
- Set an ``Accept: application/json`` header on your request
- Include ``?_json=1`` in the URL that you POST to
- Include ``"_json": 1`` in your JSON body, or ``&_json=1`` in your form encoded body
The JSON response will look like this:
.. code-block:: json
{
"ok": true,
"message": "Query executed, 1 row affected",
"redirect": "/data/add_name"
}
The ``"message"`` and ``"redirect"`` values here will take into account ``on_success_message``, ``on_success_redirect``, ``on_error_message`` and ``on_error_redirect``, if they have been set.
.. _pagination: .. _pagination:
Pagination Pagination

Wyświetl plik

@ -176,6 +176,33 @@ def test_json_post_body(canned_write_client):
assert rows == [{"rowid": 1, "name": "['Hello', 'there']"}] assert rows == [{"rowid": 1, "name": "['Hello', 'there']"}]
@pytest.mark.parametrize(
"headers,body,querystring",
(
(None, "name=NameGoesHere", "?_json=1"),
({"Accept": "application/json"}, "name=NameGoesHere", None),
(None, "name=NameGoesHere&_json=1", None),
(None, '{"name": "NameGoesHere", "_json": 1}', None),
),
)
def test_json_response(canned_write_client, headers, body, querystring):
response = canned_write_client.post(
"/data/add_name" + (querystring or ""),
body=body,
allow_redirects=False,
headers=headers,
)
assert 200 == response.status
assert response.headers["content-type"] == "application/json; charset=utf-8"
assert response.json == {
"ok": True,
"message": "Query executed, 1 row affected",
"redirect": "/data/add_name?success",
}
rows = canned_write_client.get("/data/names.json?_shape=array").json
assert rows == [{"rowid": 1, "name": "NameGoesHere"}]
def test_canned_query_permissions_on_database_page(canned_write_client): def test_canned_query_permissions_on_database_page(canned_write_client):
# Without auth only shows three queries # Without auth only shows three queries
query_names = { query_names = {
@ -196,7 +223,14 @@ def test_canned_query_permissions_on_database_page(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 == response.status assert 200 == response.status
assert [ query_names_and_private = sorted(
[
{"name": q["name"], "private": q["private"]}
for q in response.json["queries"]
],
key=lambda q: q["name"],
)
assert query_names_and_private == [
{"name": "add_name", "private": False}, {"name": "add_name", "private": False},
{"name": "add_name_specify_id", "private": False}, {"name": "add_name_specify_id", "private": False},
{"name": "canned_read", "private": False}, {"name": "canned_read", "private": False},
@ -204,13 +238,7 @@ def test_canned_query_permissions_on_database_page(canned_write_client):
{"name": "from_async_hook", "private": False}, {"name": "from_async_hook", "private": False},
{"name": "from_hook", "private": False}, {"name": "from_hook", "private": False},
{"name": "update_name", "private": False}, {"name": "update_name", "private": False},
] == sorted( ]
[
{"name": q["name"], "private": q["private"]}
for q in response.json["queries"]
],
key=lambda q: q["name"],
)
def test_canned_query_permissions(canned_write_client): def test_canned_query_permissions(canned_write_client):