datasette.urls.table(..., format="json"), closes #1035

Also improved tests for datasette.urls and added format= to some other methods
pull/1085/head
Simon Willison 2020-10-31 11:16:28 -07:00
rodzic b84cfe1b08
commit 11eb1e026f
7 zmienionych plików z 92 dodań i 40 usunięć

Wyświetl plik

@ -53,6 +53,7 @@ from .utils import (
format_bytes, format_bytes,
module_from_path, module_from_path,
parse_metadata, parse_metadata,
path_with_format,
resolve_env_secrets, resolve_env_secrets,
sqlite3, sqlite3,
to_css_class, to_css_class,
@ -1285,13 +1286,16 @@ class Urls:
def __init__(self, ds): def __init__(self, ds):
self.ds = ds self.ds = ds
def path(self, path): def path(self, path, format=None):
if path.startswith("/"): if path.startswith("/"):
path = path[1:] path = path[1:]
return self.ds.config("base_url") + path path = self.ds.config("base_url") + path
if format is not None:
path = path_with_format(path=path, format=format)
return path
def instance(self): def instance(self, format=None):
return self.path("") return self.path("", format=format)
def static(self, path): def static(self, path):
return self.path("-/static/{}".format(path)) return self.path("-/static/{}".format(path))
@ -1302,21 +1306,33 @@ class Urls:
def logout(self): def logout(self):
return self.path("-/logout") return self.path("-/logout")
def database(self, database): def database(self, database, format=None):
db = self.ds.databases[database] db = self.ds.databases[database]
if self.ds.config("hash_urls") and db.hash: if self.ds.config("hash_urls") and db.hash:
return self.path("{}-{}".format(database, db.hash[:HASH_LENGTH])) path = self.path(
"{}-{}".format(database, db.hash[:HASH_LENGTH]), format=format
)
else: else:
return self.path(database) path = self.path(database, format=format)
return path
def table(self, database, table): def table(self, database, table, format=None):
return "{}/{}".format(self.database(database), urllib.parse.quote_plus(table)) path = "{}/{}".format(self.database(database), urllib.parse.quote_plus(table))
if format is not None:
path = path_with_format(path=path, format=format)
return path
def query(self, database, query): def query(self, database, query, format=None):
return "{}/{}".format(self.database(database), urllib.parse.quote_plus(query)) path = "{}/{}".format(self.database(database), urllib.parse.quote_plus(query))
if format is not None:
path = path_with_format(path=path, format=format)
return path
def row(self, database, table, row_path): def row(self, database, table, row_path, format=None):
return "{}/{}".format(self.table(database, table), row_path) path = "{}/{}".format(self.table(database, table), row_path)
if format is not None:
path = path_with_format(path=path, format=format)
return path
def row_blob(self, database, table, row_path, column): def row_blob(self, database, table, row_path, column):
return self.table(database, table) + "/{}.blob?_blob_column={}".format( return self.table(database, table) + "/{}.blob?_blob_column={}".format(

Wyświetl plik

@ -678,9 +678,11 @@ async def resolve_table_and_format(
return table_and_format, None return table_and_format, None
def path_with_format(request, format, extra_qs=None, replace_format=None): def path_with_format(
*, request=None, path=None, format=None, extra_qs=None, replace_format=None
):
qs = extra_qs or {} qs = extra_qs or {}
path = request.path path = request.path if request else path
if replace_format and path.endswith(".{}".format(replace_format)): if replace_format and path.endswith(".{}".format(replace_format)):
path = path[: -(1 + len(replace_format))] path = path[: -(1 + len(replace_format))]
if "." in path: if "." in path:
@ -689,11 +691,11 @@ def path_with_format(request, format, extra_qs=None, replace_format=None):
path = "{}.{}".format(path, format) path = "{}.{}".format(path, format)
if qs: if qs:
extra = urllib.parse.urlencode(sorted(qs.items())) extra = urllib.parse.urlencode(sorted(qs.items()))
if request.query_string: if request and request.query_string:
path = "{}?{}&{}".format(path, request.query_string, extra) path = "{}?{}&{}".format(path, request.query_string, extra)
else: else:
path = "{}?{}".format(path, extra) path = "{}?{}".format(path, extra)
elif request.query_string: elif request and request.query_string:
path = "{}?{}".format(path, request.query_string) path = "{}?{}".format(path, request.query_string)
return path return path

Wyświetl plik

@ -333,8 +333,8 @@ class DataView(BaseView):
cell = self.ds.absolute_url( cell = self.ds.absolute_url(
request, request,
path_with_format( path_with_format(
request, request=request,
"blob", format="blob",
extra_qs={ extra_qs={
"_blob_column": column, "_blob_column": column,
"_blob_hash": hashlib.sha256( "_blob_hash": hashlib.sha256(
@ -535,11 +535,13 @@ class DataView(BaseView):
it_can_render = await await_me_maybe(it_can_render) it_can_render = await await_me_maybe(it_can_render)
if it_can_render: if it_can_render:
renderers[key] = path_with_format( renderers[key] = path_with_format(
request, key, {**url_labels_extra} request=request, format=key, extra_qs={**url_labels_extra}
) )
url_csv_args = {"_size": "max", **url_labels_extra} url_csv_args = {"_size": "max", **url_labels_extra}
url_csv = path_with_format(request, "csv", url_csv_args) url_csv = path_with_format(
request=request, format="csv", extra_qs=url_csv_args
)
url_csv_path = url_csv.split("?")[0] url_csv_path = url_csv.split("?")[0]
context = { context = {
**data, **data,

Wyświetl plik

@ -346,8 +346,8 @@ class QueryView(DataView):
) )
elif isinstance(display_value, bytes): elif isinstance(display_value, bytes):
blob_url = path_with_format( blob_url = path_with_format(
request, request=request,
"blob", format="blob",
extra_qs={ extra_qs={
"_blob_column": column, "_blob_column": column,
"_blob_hash": hashlib.sha256( "_blob_hash": hashlib.sha256(

Wyświetl plik

@ -396,10 +396,10 @@ datasette.urls
The ``datasette.urls`` object contains methods for building URLs to pages within Datasette. Plugins should use this to link to pages, since these methods take into account any :ref:`config_base_url` configuration setting that might be in effect. The ``datasette.urls`` object contains methods for building URLs to pages within Datasette. Plugins should use this to link to pages, since these methods take into account any :ref:`config_base_url` configuration setting that might be in effect.
``datasette.urls.instance()`` ``datasette.urls.instance(format=None)``
Returns the URL to the Datasette instance root page. This is usually ``"/"`` Returns the URL to the Datasette instance root page. This is usually ``"/"``.
``datasette.urls.path(path)`` ``datasette.urls.path(path, format=None)``
Takes a path and returns the full path, taking ``base_url`` into account. Takes a path and returns the full path, taking ``base_url`` into account.
For example, ``datasette.urls.path("-/logout")`` will return the path to the logout page, which will be ``"/-/logout"`` by default or ``/prefix-path/-/logout`` if ``base_url`` is set to ``/prefix-path/`` For example, ``datasette.urls.path("-/logout")`` will return the path to the logout page, which will be ``"/-/logout"`` by default or ``/prefix-path/-/logout`` if ``base_url`` is set to ``/prefix-path/``
@ -423,13 +423,13 @@ The ``datasette.urls`` object contains methods for building URLs to pages within
``datasette.url.static_plugins("datasette_cluster_map", "datasette-cluster-map.js")`` would return ``"/-/static-plugins/datasette_cluster_map/datasette-cluster-map.js"`` ``datasette.url.static_plugins("datasette_cluster_map", "datasette-cluster-map.js")`` would return ``"/-/static-plugins/datasette_cluster_map/datasette-cluster-map.js"``
``datasette.urls.database(database_name)`` ``datasette.urls.database(database_name, format=None)``
Returns the URL to a database page, for example ``"/fixtures"`` Returns the URL to a database page, for example ``"/fixtures"``
``datasette.urls.table(database_name, table_name)`` ``datasette.urls.table(database_name, table_name, format=None)``
Returns the URL to a table page, for example ``"/fixtures/facetable"`` Returns the URL to a table page, for example ``"/fixtures/facetable"``
``datasette.urls.query(database_name, query_name)`` ``datasette.urls.query(database_name, query_name, format=None)``
Returns the URL to a query page, for example ``"/fixtures/pragma_cache_size"`` Returns the URL to a query page, for example ``"/fixtures/pragma_cache_size"``
These functions can be accessed via the ``{{ urls }}`` object in Datasette templates, for example: These functions can be accessed via the ``{{ urls }}`` object in Datasette templates, for example:
@ -441,6 +441,8 @@ These functions can be accessed via the ``{{ urls }}`` object in Datasette templ
<a href="{{ urls.table("fixtures", "facetable") }}">facetable table</a> <a href="{{ urls.table("fixtures", "facetable") }}">facetable table</a>
<a href="{{ urls.query("fixtures", "pragma_cache_size") }}">pragma_cache_size query</a> <a href="{{ urls.query("fixtures", "pragma_cache_size") }}">pragma_cache_size query</a>
Use the ``format="json"`` (or ``"csv"`` or other formats supported by plugins) arguments to get back URLs to the JSON representation. This is usually the path with ``.json`` added on the end, but it may use ``?_format=json`` in cases where the path already includes ``.json``, for example a URL to a table named ``table.json``.
.. _internals_database: .. _internals_database:
Database class Database class

Wyświetl plik

@ -82,18 +82,44 @@ def test_logout(ds, base_url, expected):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"base_url,expected", "base_url,format,expected",
[ [
("/", "/:memory:"), ("/", None, "/:memory:"),
("/prefix/", "/prefix/:memory:"), ("/prefix/", None, "/prefix/:memory:"),
("/", "json", "/:memory:.json"),
], ],
) )
def test_database(ds, base_url, expected): def test_database(ds, base_url, format, expected):
ds._config["base_url"] = base_url ds._config["base_url"] = base_url
assert ds.urls.database(":memory:") == expected assert ds.urls.database(":memory:", format=format) == expected
# Do table and query while we are here
assert ds.urls.table(":memory:", "name") == expected + "/name"
assert ds.urls.query(":memory:", "name") == expected + "/name" @pytest.mark.parametrize(
"base_url,name,format,expected",
[
("/", "name", None, "/:memory:/name"),
("/prefix/", "name", None, "/prefix/:memory:/name"),
("/", "name", "json", "/:memory:/name.json"),
("/", "name.json", "json", "/:memory:/name.json?_format=json"),
],
)
def test_table_and_query(ds, base_url, name, format, expected):
ds._config["base_url"] = base_url
assert ds.urls.table(":memory:", name, format=format) == expected
assert ds.urls.query(":memory:", name, format=format) == expected
@pytest.mark.parametrize(
"base_url,format,expected",
[
("/", None, "/:memory:/facetable/1"),
("/prefix/", None, "/prefix/:memory:/facetable/1"),
("/", "json", "/:memory:/facetable/1.json"),
],
)
def test_row(ds, base_url, format, expected):
ds._config["base_url"] = base_url
assert ds.urls.row(":memory:", "facetable", "1", format=format) == expected
@pytest.mark.parametrize("base_url", ["/", "/prefix/"]) @pytest.mark.parametrize("base_url", ["/", "/prefix/"])

Wyświetl plik

@ -382,15 +382,19 @@ def test_table_columns():
) )
def test_path_with_format(path, format, extra_qs, expected): def test_path_with_format(path, format, extra_qs, expected):
request = Request.fake(path) request = Request.fake(path)
actual = utils.path_with_format(request, format, extra_qs) actual = utils.path_with_format(request=request, format=format, extra_qs=extra_qs)
assert expected == actual assert expected == actual
def test_path_with_format_replace_format(): def test_path_with_format_replace_format():
request = Request.fake("/foo/bar.csv") request = Request.fake("/foo/bar.csv")
assert utils.path_with_format(request, "blob") == "/foo/bar.csv?_format=blob"
assert ( assert (
utils.path_with_format(request, "blob", replace_format="csv") == "/foo/bar.blob" utils.path_with_format(request=request, format="blob")
== "/foo/bar.csv?_format=blob"
)
assert (
utils.path_with_format(request=request, format="blob", replace_format="csv")
== "/foo/bar.blob"
) )