mirror of
https://github.com/simonw/datasette.git
synced 2026-05-31 14:16:59 +02:00
Fix double-prefixed export links with base_url
Use the router-stripped route_path when building request-derived export URLs, so table, row, and query JSON/CSV links do not apply base_url twice. Keep urls.path() behavior unchanged, and add coverage for both /prefix/ exports and a /data/ base_url with a data database. Closes #2759
This commit is contained in:
parent
81a4df8a3e
commit
d657fb4315
7 changed files with 118 additions and 11 deletions
|
|
@ -837,7 +837,8 @@ def path_with_format(
|
|||
*, request=None, path=None, format=None, extra_qs=None, replace_format=None
|
||||
):
|
||||
qs = extra_qs or {}
|
||||
path = request.path if request else path
|
||||
if path is None and request:
|
||||
path = request.path
|
||||
if replace_format and path.endswith(f".{replace_format}"):
|
||||
path = path[: -(1 + len(replace_format))]
|
||||
if "." in path:
|
||||
|
|
|
|||
|
|
@ -153,7 +153,13 @@ class BaseView:
|
|||
if self.has_json_alternate:
|
||||
alternate_url_json = self.ds.absolute_url(
|
||||
request,
|
||||
self.ds.urls.path(path_with_format(request=request, format="json")),
|
||||
self.ds.urls.path(
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="json",
|
||||
)
|
||||
),
|
||||
)
|
||||
template_context["alternate_url_json"] = alternate_url_json
|
||||
headers.update(
|
||||
|
|
@ -347,13 +353,21 @@ class DataView(BaseView):
|
|||
if it_can_render:
|
||||
renderers[key] = self.ds.urls.path(
|
||||
path_with_format(
|
||||
request=request, format=key, extra_qs={**url_labels_extra}
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format=key,
|
||||
extra_qs={**url_labels_extra},
|
||||
)
|
||||
)
|
||||
|
||||
url_csv_args = {"_size": "max", **url_labels_extra}
|
||||
url_csv = self.ds.urls.path(
|
||||
path_with_format(request=request, format="csv", extra_qs=url_csv_args)
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="csv",
|
||||
extra_qs=url_csv_args,
|
||||
)
|
||||
)
|
||||
url_csv_path = url_csv.split("?")[0]
|
||||
context = {
|
||||
|
|
|
|||
|
|
@ -157,7 +157,13 @@ class DatabaseView(View):
|
|||
assert format_ == "html"
|
||||
alternate_url_json = datasette.absolute_url(
|
||||
request,
|
||||
datasette.urls.path(path_with_format(request=request, format="json")),
|
||||
datasette.urls.path(
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="json",
|
||||
)
|
||||
),
|
||||
)
|
||||
templates = (f"database-{to_css_class(database)}.html", "database.html")
|
||||
environment = datasette.get_jinja_environment(request)
|
||||
|
|
@ -744,7 +750,13 @@ class QueryView(View):
|
|||
template = environment.select_template(templates)
|
||||
alternate_url_json = datasette.absolute_url(
|
||||
request,
|
||||
datasette.urls.path(path_with_format(request=request, format="json")),
|
||||
datasette.urls.path(
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="json",
|
||||
)
|
||||
),
|
||||
)
|
||||
data = {}
|
||||
headers.update(
|
||||
|
|
@ -776,7 +788,11 @@ class QueryView(View):
|
|||
it_can_render = await await_me_maybe(it_can_render)
|
||||
if it_can_render:
|
||||
renderers[key] = datasette.urls.path(
|
||||
path_with_format(request=request, format=key)
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format=key,
|
||||
)
|
||||
)
|
||||
|
||||
allow_execute_sql = await datasette.allowed(
|
||||
|
|
@ -905,7 +921,10 @@ class QueryView(View):
|
|||
renderers=renderers,
|
||||
url_csv=datasette.urls.path(
|
||||
path_with_format(
|
||||
request=request, format="csv", extra_qs={"_size": "max"}
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="csv",
|
||||
extra_qs={"_size": "max"},
|
||||
)
|
||||
),
|
||||
show_hide_hidden=markupsafe.Markup(show_hide_hidden),
|
||||
|
|
|
|||
|
|
@ -1072,7 +1072,13 @@ async def table_view_traced(datasette, request):
|
|||
template = environment.select_template(templates)
|
||||
alternate_url_json = datasette.absolute_url(
|
||||
request,
|
||||
datasette.urls.path(path_with_format(request=request, format="json")),
|
||||
datasette.urls.path(
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="json",
|
||||
)
|
||||
),
|
||||
)
|
||||
headers.update(
|
||||
{
|
||||
|
|
@ -1878,7 +1884,10 @@ async def table_view_data(
|
|||
if it_can_render:
|
||||
renderers[key] = datasette.urls.path(
|
||||
path_with_format(
|
||||
request=request, format=key, extra_qs={**url_labels_extra}
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format=key,
|
||||
extra_qs={**url_labels_extra},
|
||||
)
|
||||
)
|
||||
return renderers
|
||||
|
|
@ -2042,7 +2051,12 @@ async def table_view_data(
|
|||
url_labels_extra = {"_labels": "on"}
|
||||
url_csv_args = {"_size": "max", **url_labels_extra}
|
||||
url_csv = datasette.urls.path(
|
||||
path_with_format(request=request, format="csv", extra_qs=url_csv_args)
|
||||
path_with_format(
|
||||
request=request,
|
||||
path=request.scope.get("route_path"),
|
||||
format="csv",
|
||||
extra_qs=url_csv_args,
|
||||
)
|
||||
)
|
||||
url_csv_path = url_csv.split("?")[0]
|
||||
data.update(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ def ds():
|
|||
("/prefix/", "/", "/prefix/"),
|
||||
("/prefix/", "/foo", "/prefix/foo"),
|
||||
("/prefix/", "foo", "/prefix/foo"),
|
||||
("/data/", "/data", "/data/data"),
|
||||
("/data/", "/data/foo", "/data/data/foo"),
|
||||
],
|
||||
)
|
||||
def test_path(ds, base_url, path, expected):
|
||||
|
|
|
|||
|
|
@ -510,6 +510,57 @@ async def test_table_csv_json_export_interface(ds_client):
|
|||
] == inputs
|
||||
|
||||
|
||||
def test_base_url_export_links_are_not_double_prefixed(app_client_base_url_prefix):
|
||||
for path, expected_json, expected_csv in (
|
||||
(
|
||||
"/prefix/fixtures/simple_primary_key?id__gt=2",
|
||||
"/prefix/fixtures/simple_primary_key.json?id__gt=2",
|
||||
"/prefix/fixtures/simple_primary_key.csv?id__gt=2&_size=max",
|
||||
),
|
||||
(
|
||||
"/prefix/fixtures/simple_primary_key/1",
|
||||
"/prefix/fixtures/simple_primary_key/1.json",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"/prefix/fixtures/-/query?sql=select+1",
|
||||
"/prefix/fixtures/-/query.json?sql=select+1",
|
||||
"/prefix/fixtures/-/query.csv?sql=select+1&_size=max",
|
||||
),
|
||||
):
|
||||
response = app_client_base_url_prefix.get(path)
|
||||
assert response.status_code == 200
|
||||
soup = Soup(response.text, "html.parser")
|
||||
export_links = soup.find("p", {"class": "export-links"}) or next(
|
||||
p for p in soup.find_all("p") if "This data as" in p.get_text()
|
||||
)
|
||||
hrefs = [a["href"] for a in export_links.find_all("a", href=True)]
|
||||
assert expected_json in hrefs
|
||||
if expected_csv:
|
||||
assert expected_csv in hrefs
|
||||
assert not [href for href in hrefs if href.startswith("/prefix/prefix/")]
|
||||
assert response.headers["link"] == (
|
||||
f"<http://localhost{expected_json}>; "
|
||||
'rel="alternate"; type="application/json+datasette"'
|
||||
)
|
||||
|
||||
|
||||
def test_base_url_export_links_when_database_matches_prefix():
|
||||
with make_app_client(settings={"base_url": "/data/"}, filename="data.db") as client:
|
||||
response = client.get("/data/data/simple_primary_key?id__gt=2")
|
||||
assert response.status_code == 200
|
||||
export_links = Soup(response.text, "html.parser").find(
|
||||
"p", {"class": "export-links"}
|
||||
)
|
||||
hrefs = [a["href"] for a in export_links.find_all("a", href=True)]
|
||||
assert "/data/data/simple_primary_key.json?id__gt=2" in hrefs
|
||||
assert "/data/data/simple_primary_key.csv?id__gt=2&_size=max" in hrefs
|
||||
assert response.headers["link"] == (
|
||||
"<http://localhost/data/data/simple_primary_key.json?id__gt=2>; "
|
||||
'rel="alternate"; type="application/json+datasette"'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_csv_json_export_links_include_labels_if_foreign_keys(ds_client):
|
||||
response = await ds_client.get("/fixtures/facetable")
|
||||
|
|
|
|||
|
|
@ -453,6 +453,12 @@ def test_path_with_format(path, format, extra_qs, expected):
|
|||
assert expected == actual
|
||||
|
||||
|
||||
def test_path_with_format_can_override_request_path():
|
||||
request = Request.fake("/prefix/foo?x=1")
|
||||
actual = utils.path_with_format(request=request, path="/foo", format="json")
|
||||
assert "/foo.json?x=1" == actual
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bytes,expected",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue