mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
base_url configuration setting, refs #394
This commit is contained in:
parent
a498d0fe65
commit
0b37a104fd
11 changed files with 75 additions and 9 deletions
|
|
@ -135,7 +135,9 @@ CONFIG_OPTIONS = (
|
||||||
False,
|
False,
|
||||||
"Allow display of template debug information with ?_context=1",
|
"Allow display of template debug information with ?_context=1",
|
||||||
),
|
),
|
||||||
|
ConfigOption("base_url", "/", "Datasette URLs should use this base"),
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_CONFIG = {option.name: option.default for option in CONFIG_OPTIONS}
|
DEFAULT_CONFIG = {option.name: option.default for option in CONFIG_OPTIONS}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -573,6 +575,7 @@ class Datasette:
|
||||||
"format_bytes": format_bytes,
|
"format_bytes": format_bytes,
|
||||||
"extra_css_urls": self._asset_urls("extra_css_urls", template, context),
|
"extra_css_urls": self._asset_urls("extra_css_urls", template, context),
|
||||||
"extra_js_urls": self._asset_urls("extra_js_urls", template, context),
|
"extra_js_urls": self._asset_urls("extra_js_urls", template, context),
|
||||||
|
"base_url": self.config("base_url"),
|
||||||
},
|
},
|
||||||
**extra_template_vars,
|
**extra_template_vars,
|
||||||
}
|
}
|
||||||
|
|
@ -736,6 +739,13 @@ class DatasetteRouter(AsgiRouter):
|
||||||
self.ds = datasette
|
self.ds = datasette
|
||||||
super().__init__(routes)
|
super().__init__(routes)
|
||||||
|
|
||||||
|
async def route_path(self, scope, receive, send, path):
|
||||||
|
# Strip off base_url if present before routing
|
||||||
|
base_url = self.ds.config("base_url")
|
||||||
|
if base_url != "/" and path.startswith(base_url):
|
||||||
|
path = "/" + path[len(base_url) :]
|
||||||
|
return await super().route_path(scope, receive, send, path)
|
||||||
|
|
||||||
async def handle_404(self, scope, receive, send):
|
async def handle_404(self, scope, receive, send):
|
||||||
# If URL has a trailing slash, redirect to URL without it
|
# If URL has a trailing slash, redirect to URL without it
|
||||||
path = scope.get("raw_path", scope["path"].encode("utf8"))
|
path = scope.get("raw_path", scope["path"].encode("utf8"))
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class Config(click.ParamType):
|
||||||
if ":" not in config:
|
if ":" not in config:
|
||||||
self.fail('"{}" should be name:value'.format(config), param, ctx)
|
self.fail('"{}" should be name:value'.format(config), param, ctx)
|
||||||
return
|
return
|
||||||
name, value = config.split(":")
|
name, value = config.split(":", 1)
|
||||||
if name not in DEFAULT_CONFIG:
|
if name not in DEFAULT_CONFIG:
|
||||||
self.fail(
|
self.fail(
|
||||||
"{} is not a valid option (--help-config to see all)".format(name),
|
"{} is not a valid option (--help-config to see all)".format(name),
|
||||||
|
|
@ -50,6 +50,8 @@ class Config(click.ParamType):
|
||||||
self.fail('"{}" should be an integer'.format(name), param, ctx)
|
self.fail('"{}" should be an integer'.format(name), param, ctx)
|
||||||
return
|
return
|
||||||
return name, int(value)
|
return name, int(value)
|
||||||
|
elif isinstance(default, str):
|
||||||
|
return name, value
|
||||||
else:
|
else:
|
||||||
# Should never happen:
|
# Should never happen:
|
||||||
self.fail("Invalid option")
|
self.fail("Invalid option")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<p class="crumbs">
|
<p class="crumbs">
|
||||||
<a href="/">home</a>
|
<a href="{{ base_url }}">home</a>
|
||||||
</p>
|
</p>
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<p class="crumbs">
|
<p class="crumbs">
|
||||||
<a href="/">home</a> /
|
<a href="{{ base_url }}">home</a> /
|
||||||
<a href="{{ database_url(database) }}">{{ database }}</a> /
|
<a href="{{ database_url(database) }}">{{ database }}</a> /
|
||||||
<a href="{{ database_url(database) }}/{{ table|quote_plus }}">{{ table }}</a>
|
<a href="{{ database_url(database) }}/{{ table|quote_plus }}">{{ table }}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<p class="crumbs">
|
<p class="crumbs">
|
||||||
<a href="/">home</a> /
|
<a href="{{ base_url }}">home</a> /
|
||||||
<a href="{{ database_url(database) }}">{{ database }}</a>
|
<a href="{{ database_url(database) }}">{{ database }}</a>
|
||||||
</p>
|
</p>
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,9 @@ class AsgiRouter:
|
||||||
raw_path = scope.get("raw_path")
|
raw_path = scope.get("raw_path")
|
||||||
if raw_path:
|
if raw_path:
|
||||||
path = raw_path.decode("ascii")
|
path = raw_path.decode("ascii")
|
||||||
|
return await self.route_path(scope, receive, send, path)
|
||||||
|
|
||||||
|
async def route_path(self, scope, receive, send, path):
|
||||||
for regex, view in self.routes:
|
for regex, view in self.routes:
|
||||||
match = regex.match(path)
|
match = regex.match(path)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,11 @@ class BaseView(AsgiView):
|
||||||
|
|
||||||
def database_url(self, database):
|
def database_url(self, database):
|
||||||
db = self.ds.databases[database]
|
db = self.ds.databases[database]
|
||||||
|
base_url = self.ds.config("base_url")
|
||||||
if self.ds.config("hash_urls") and db.hash:
|
if self.ds.config("hash_urls") and db.hash:
|
||||||
return "/{}-{}".format(database, db.hash[:HASH_LENGTH])
|
return "{}{}-{}".format(base_url, database, db.hash[:HASH_LENGTH])
|
||||||
else:
|
else:
|
||||||
return "/{}".format(database)
|
return "{}{}".format(base_url, database)
|
||||||
|
|
||||||
def database_color(self, database):
|
def database_color(self, database):
|
||||||
return "ff0000"
|
return "ff0000"
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ from datasette.filters import Filters
|
||||||
from .base import DataView, DatasetteError, ureg
|
from .base import DataView, DatasetteError, ureg
|
||||||
|
|
||||||
LINK_WITH_LABEL = (
|
LINK_WITH_LABEL = (
|
||||||
'<a href="/{database}/{table}/{link_id}">{label}</a> <em>{id}</em>'
|
'<a href="{base_url}{database}/{table}/{link_id}">{label}</a> <em>{id}</em>'
|
||||||
)
|
)
|
||||||
LINK_WITH_VALUE = '<a href="/{database}/{table}/{link_id}">{id}</a>'
|
LINK_WITH_VALUE = '<a href="{base_url}{database}/{table}/{link_id}">{id}</a>'
|
||||||
|
|
||||||
|
|
||||||
class Row:
|
class Row:
|
||||||
|
|
@ -100,6 +100,7 @@ class RowTableShared(DataView):
|
||||||
}
|
}
|
||||||
|
|
||||||
cell_rows = []
|
cell_rows = []
|
||||||
|
base_url = self.ds.config("base_url")
|
||||||
for row in rows:
|
for row in rows:
|
||||||
cells = []
|
cells = []
|
||||||
# Unless we are a view, the first column is a link - either to the rowid
|
# Unless we are a view, the first column is a link - either to the rowid
|
||||||
|
|
@ -113,7 +114,8 @@ class RowTableShared(DataView):
|
||||||
"is_special_link_column": is_special_link_column,
|
"is_special_link_column": is_special_link_column,
|
||||||
"raw": pk_path,
|
"raw": pk_path,
|
||||||
"value": jinja2.Markup(
|
"value": jinja2.Markup(
|
||||||
'<a href="/{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'.format(
|
'<a href="{base_url}{database}/{table}/{flat_pks_quoted}">{flat_pks}</a>'.format(
|
||||||
|
base_url=base_url,
|
||||||
database=database,
|
database=database,
|
||||||
table=urllib.parse.quote_plus(table),
|
table=urllib.parse.quote_plus(table),
|
||||||
flat_pks=str(jinja2.escape(pk_path)),
|
flat_pks=str(jinja2.escape(pk_path)),
|
||||||
|
|
@ -159,6 +161,7 @@ class RowTableShared(DataView):
|
||||||
display_value = jinja2.Markup(
|
display_value = jinja2.Markup(
|
||||||
link_template.format(
|
link_template.format(
|
||||||
database=database,
|
database=database,
|
||||||
|
base_url=base_url,
|
||||||
table=urllib.parse.quote_plus(other_table),
|
table=urllib.parse.quote_plus(other_table),
|
||||||
link_id=urllib.parse.quote_plus(str(value)),
|
link_id=urllib.parse.quote_plus(str(value)),
|
||||||
id=str(jinja2.escape(value)),
|
id=str(jinja2.escape(value)),
|
||||||
|
|
|
||||||
|
|
@ -228,3 +228,16 @@ Some examples:
|
||||||
* https://latest.datasette.io/?_context=1
|
* https://latest.datasette.io/?_context=1
|
||||||
* https://latest.datasette.io/fixtures?_context=1
|
* https://latest.datasette.io/fixtures?_context=1
|
||||||
* https://latest.datasette.io/fixtures/roadside_attractions?_context=1
|
* https://latest.datasette.io/fixtures/roadside_attractions?_context=1
|
||||||
|
|
||||||
|
.. _config_base_url:
|
||||||
|
|
||||||
|
base_url
|
||||||
|
--------
|
||||||
|
|
||||||
|
If you are running Datasette behind a proxy, it may be useful to change the root URL used for the Datasette instance.
|
||||||
|
|
||||||
|
For example, if you are sending traffic from `https://www.example.com/tools/datasette/` through to a proxied Datasette instance you may wish Datasette to use `/tools/datasette/` as its root URL.
|
||||||
|
|
||||||
|
You can do that like so::
|
||||||
|
|
||||||
|
datasette mydatabase.db --config base_url:/tools/datasette/
|
||||||
|
|
|
||||||
|
|
@ -1307,6 +1307,7 @@ def test_config_json(app_client):
|
||||||
"force_https_urls": False,
|
"force_https_urls": False,
|
||||||
"hash_urls": False,
|
"hash_urls": False,
|
||||||
"template_debug": False,
|
"template_debug": False,
|
||||||
|
"base_url": "/",
|
||||||
} == response.json
|
} == response.json
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1157,3 +1157,36 @@ def test_metadata_sort_desc(app_client):
|
||||||
table = Soup(response.body, "html.parser").find("table")
|
table = Soup(response.body, "html.parser").find("table")
|
||||||
rows = [[str(td) for td in tr.select("td")] for tr in table.select("tbody tr")]
|
rows = [[str(td) for td in tr.select("td")] for tr in table.select("tbody tr")]
|
||||||
assert list(reversed(expected)) == rows
|
assert list(reversed(expected)) == rows
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("base_url", ["/prefix/", "https://example.com/"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path",
|
||||||
|
[
|
||||||
|
"/",
|
||||||
|
"/fixtures",
|
||||||
|
"/fixtures/compound_three_primary_keys",
|
||||||
|
"/fixtures/compound_three_primary_keys/a,a,a",
|
||||||
|
"/fixtures/paginated_view",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_base_url_config(base_url, path):
|
||||||
|
for client in make_app_client(config={"base_url": base_url}):
|
||||||
|
response = client.get(base_url + path.lstrip("/"))
|
||||||
|
soup = Soup(response.body, "html.parser")
|
||||||
|
for a in soup.findAll("a"):
|
||||||
|
href = a["href"]
|
||||||
|
if not href.startswith("#") and href not in {
|
||||||
|
"https://github.com/simonw/datasette",
|
||||||
|
"https://github.com/simonw/datasette/blob/master/LICENSE",
|
||||||
|
"https://github.com/simonw/datasette/blob/master/tests/fixtures.py",
|
||||||
|
}:
|
||||||
|
# If this has been made absolute it may start http://localhost/
|
||||||
|
if href.startswith("http://localhost/"):
|
||||||
|
href = href[len("http://localost/") :]
|
||||||
|
assert href.startswith(base_url), {
|
||||||
|
"base_url": base_url,
|
||||||
|
"path": path,
|
||||||
|
"href_in_document": href,
|
||||||
|
"link_parent": str(a.parent),
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue