Skip to content

Commit

Permalink
base_url configuration setting, closes #394
Browse files Browse the repository at this point in the history
* base_url configuration setting
* base_url works for static assets as well
  • Loading branch information
simonw committed Mar 25, 2020
1 parent 2a36dfa commit 7656fd6
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 28 deletions.
10 changes: 10 additions & 0 deletions datasette/app.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@
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}




Expand Down Expand Up @@ -573,6 +575,7 @@ async def render_template(
"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,
} }
Expand Down Expand Up @@ -736,6 +739,13 @@ def __init__(self, datasette, routes):
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"))
Expand Down
4 changes: 3 additions & 1 deletion datasette/cli.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def convert(self, config, param, ctx):
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),
Expand All @@ -50,6 +50,8 @@ def convert(self, config, param, ctx):
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")
Expand Down
8 changes: 4 additions & 4 deletions datasette/templates/_codemirror.html
Original file line number Original file line Diff line number Diff line change
@@ -1,7 +1,7 @@
<script src="/-/static/sql-formatter-2.3.3.min.js" defer></script> <script src="{{ base_url }}-/static/sql-formatter-2.3.3.min.js" defer></script>
<script src="/-/static/codemirror-5.31.0.js"></script> <script src="{{ base_url }}-/static/codemirror-5.31.0.js"></script>
<link rel="stylesheet" href="/-/static/codemirror-5.31.0-min.css" /> <link rel="stylesheet" href="{{ base_url }}-/static/codemirror-5.31.0-min.css" />
<script src="/-/static/codemirror-5.31.0-sql.min.js"></script> <script src="{{ base_url }}-/static/codemirror-5.31.0-sql.min.js"></script>
<style> <style>
.CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; } .CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; }
.CodeMirror-scroll { max-height: 200px; } .CodeMirror-scroll { max-height: 200px; }
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/base.html
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/-/static/app.css?{{ app_css_hash }}"> <link rel="stylesheet" href="{{ base_url }}-/static/app.css?{{ app_css_hash }}">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% for url in extra_css_urls %} {% for url in extra_css_urls %}
<link rel="stylesheet" href="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}> <link rel="stylesheet" href="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}>
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/database.html
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/row.html
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion datasette/templates/table.html
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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() }}
Expand Down
3 changes: 3 additions & 0 deletions datasette/utils/asgi.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ async def __call__(self, scope, receive, send):
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:
Expand Down
5 changes: 3 additions & 2 deletions datasette/views/base.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ async def head(self, *args, **kwargs):


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"
Expand Down
9 changes: 6 additions & 3 deletions datasette/views/table.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
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>&nbsp;<em>{id}</em>' '<a href="{base_url}{database}/{table}/{link_id}">{label}</a>&nbsp;<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:
Expand Down Expand Up @@ -100,6 +100,7 @@ async def display_columns_and_rows(
} }


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
Expand All @@ -113,7 +114,8 @@ async def display_columns_and_rows(
"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)),
Expand Down Expand Up @@ -159,6 +161,7 @@ async def display_columns_and_rows(
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)),
Expand Down
13 changes: 13 additions & 0 deletions docs/config.rst
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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/
10 changes: 5 additions & 5 deletions tests/fixtures.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def prepare_connection_args():
@hookimpl @hookimpl
def extra_css_urls(template, database, table, datasette): def extra_css_urls(template, database, table, datasette):
return ['https://example.com/{}/extra-css-urls-demo.css'.format( return ['https://plugin-example.com/{}/extra-css-urls-demo.css'.format(
base64.b64encode(json.dumps({ base64.b64encode(json.dumps({
"template": template, "template": template,
"database": database, "database": database,
Expand All @@ -363,9 +363,9 @@ def extra_css_urls(template, database, table, datasette):
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [{ return [{
'url': 'https://example.com/jquery.js', 'url': 'https://plugin-example.com/jquery.js',
'sri': 'SRIHASH', 'sri': 'SRIHASH',
}, 'https://example.com/plugin1.js'] }, 'https://plugin-example.com/plugin1.js']
@hookimpl @hookimpl
Expand Down Expand Up @@ -421,9 +421,9 @@ def extra_template_vars(template, database, table, view_name, request, datasette
@hookimpl @hookimpl
def extra_js_urls(): def extra_js_urls():
return [{ return [{
'url': 'https://example.com/jquery.js', 'url': 'https://plugin-example.com/jquery.js',
'sri': 'SRIHASH', 'sri': 'SRIHASH',
}, 'https://example.com/plugin2.js'] }, 'https://plugin-example.com/plugin2.js']
@hookimpl @hookimpl
Expand Down
1 change: 1 addition & 0 deletions tests/test_api.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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




Expand Down
43 changes: 43 additions & 0 deletions tests/test_html.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -1157,3 +1157,46 @@ 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 el in soup.findAll(["a", "link", "script"]):
if "href" in el.attrs:
href = el["href"]
elif "src" in el.attrs:
href = el["src"]
else:
continue # Could be a <script>...</script>
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",
}
and not href.startswith("https://plugin-example.com/")
):
# 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_or_src": href,
"element_parent": str(el.parent),
}
18 changes: 9 additions & 9 deletions tests/test_plugins.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_plugin_extra_js_urls(app_client):
== { == {
"integrity": "SRIHASH", "integrity": "SRIHASH",
"crossorigin": "anonymous", "crossorigin": "anonymous",
"src": "https://example.com/jquery.js", "src": "https://plugin-example.com/jquery.js",
} }
] ]


Expand All @@ -74,24 +74,24 @@ def test_plugins_with_duplicate_js_urls(app_client):
response = app_client.get("/fixtures") response = app_client.get("/fixtures")
# This test is a little tricky, as if the user has any other plugins in # This test is a little tricky, as if the user has any other plugins in
# their current virtual environment those may affect what comes back too. # their current virtual environment those may affect what comes back too.
# What matters is that https://example.com/jquery.js is only there once # What matters is that https://plugin-example.com/jquery.js is only there once
# and it comes before plugin1.js and plugin2.js which could be in either # and it comes before plugin1.js and plugin2.js which could be in either
# order # order
scripts = Soup(response.body, "html.parser").findAll("script") scripts = Soup(response.body, "html.parser").findAll("script")
srcs = [s["src"] for s in scripts if s.get("src")] srcs = [s["src"] for s in scripts if s.get("src")]
# No duplicates allowed: # No duplicates allowed:
assert len(srcs) == len(set(srcs)) assert len(srcs) == len(set(srcs))
# jquery.js loaded once: # jquery.js loaded once:
assert 1 == srcs.count("https://example.com/jquery.js") assert 1 == srcs.count("https://plugin-example.com/jquery.js")
# plugin1.js and plugin2.js are both there: # plugin1.js and plugin2.js are both there:
assert 1 == srcs.count("https://example.com/plugin1.js") assert 1 == srcs.count("https://plugin-example.com/plugin1.js")
assert 1 == srcs.count("https://example.com/plugin2.js") assert 1 == srcs.count("https://plugin-example.com/plugin2.js")
# jquery comes before them both # jquery comes before them both
assert srcs.index("https://example.com/jquery.js") < srcs.index( assert srcs.index("https://plugin-example.com/jquery.js") < srcs.index(
"https://example.com/plugin1.js" "https://plugin-example.com/plugin1.js"
) )
assert srcs.index("https://example.com/jquery.js") < srcs.index( assert srcs.index("https://plugin-example.com/jquery.js") < srcs.index(
"https://example.com/plugin2.js" "https://plugin-example.com/plugin2.js"
) )




Expand Down

0 comments on commit 7656fd6

Please sign in to comment.