diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 9d189459..2dff9667 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -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: diff --git a/datasette/views/base.py b/datasette/views/base.py index e4c1c738..2e2a5443 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -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 = { diff --git a/datasette/views/database.py b/datasette/views/database.py index b4a964f1..3e3b05e3 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -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), diff --git a/datasette/views/table.py b/datasette/views/table.py index da69c6b5..4df1e1b4 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -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( diff --git a/tests/test_internals_urls.py b/tests/test_internals_urls.py index d60aafcf..fc7b0313 100644 --- a/tests/test_internals_urls.py +++ b/tests/test_internals_urls.py @@ -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): diff --git a/tests/test_table_html.py b/tests/test_table_html.py index 86b9a4eb..2e671d55 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -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"; " + '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"] == ( + "; " + '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") diff --git a/tests/test_utils.py b/tests/test_utils.py index e83eed7a..f6de3b46 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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", [