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:
Simon Willison 2026-05-30 22:40:45 -07:00
commit d657fb4315
7 changed files with 118 additions and 11 deletions

View file

@ -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):

View file

@ -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")

View file

@ -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",
[