Skip to content

Commit

Permalink
New datasette.urls URL builders, refs #904
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Oct 20, 2020
1 parent c440ffc commit 310c3a3
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 44 deletions.
32 changes: 31 additions & 1 deletion datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
resolve_env_secrets,
sqlite3,
to_css_class,
SpatialiteNotFound,
HASH_LENGTH,
)
from .utils.asgi import (
AsgiLifespan,
Expand Down Expand Up @@ -321,6 +321,10 @@ def __init__(
self._root_token = secrets.token_hex(32)
self.client = DatasetteClient(self)

@property
def urls(self):
return Urls(self)

async def invoke_startup(self):
for hook in pm.hook.startup(datasette=self):
await await_me_maybe(hook)
Expand Down Expand Up @@ -748,6 +752,7 @@ async def render_template(
template_context = {
**context,
**{
"urls": self.urls,
"actor": request.actor if request else None,
"display_actor": display_actor,
"show_logout": request is not None and "ds_actor" in request.cookies,
Expand Down Expand Up @@ -1259,3 +1264,28 @@ async def delete(self, path, **kwargs):
async def request(self, method, path, **kwargs):
async with httpx.AsyncClient(app=self.app) as client:
return await client.request(method, self._fix(path), **kwargs)


class Urls:
def __init__(self, ds):
self.ds = ds

def instance(self):
return self.ds.config("base_url")

def static(self, path):
return "{}-/static/{}".format(self.instance(), path)

def database(self, database):
db = self.ds.databases[database]
base_url = self.ds.config("base_url")
if self.ds.config("hash_urls") and db.hash:
return "{}{}-{}".format(base_url, database, db.hash[:HASH_LENGTH])
else:
return "{}{}".format(base_url, database)

def table(self, database, table):
return "{}/{}".format(self.database(database), urllib.parse.quote_plus(table))

def query(self, database, query):
return "{}/{}".format(self.database(database), urllib.parse.quote_plus(query))
14 changes: 7 additions & 7 deletions datasette/templates/database.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

{% block nav %}
<p class="crumbs">
<a href="{{ base_url }}">home</a>
<a href="{{ urls.instance() }}">home</a>
</p>
{{ super() }}
{% endblock %}
Expand All @@ -23,7 +23,7 @@ <h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color(databa
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

{% if allow_execute_sql %}
<form class="sql" action="{{ database_url(database) }}" method="get">
<form class="sql" action="{{ urls.database(database) }}" method="get">
<h3>Custom SQL query</h3>
<p><textarea id="sql-editor" name="sql">{% if tables %}select * from {{ tables[0].name|escape_sqlite }}{% else %}select sqlite_version(){% endif %}</textarea></p>
<p>
Expand All @@ -36,22 +36,22 @@ <h3>Custom SQL query</h3>
{% for table in tables %}
{% if show_hidden or not table.hidden %}
<div class="db-table">
<h2><a href="{{ database_url(database) }}/{{ table.name|quote_plus }}">{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if table.hidden %}<em> (hidden)</em>{% endif %}</h2>
<h2><a href="{{ urls.table(database, table.name) }}">{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if table.hidden %}<em> (hidden)</em>{% endif %}</h2>
<p><em>{% for column in table.columns[:9] %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}{% if table.columns|length > 9 %}...{% endif %}</em></p>
<p>{% if table.count is none %}Many rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}</p>
</div>
{% endif %}
{% endfor %}

{% if hidden_count and not show_hidden %}
<p>... and <a href="{{ database_url(database) }}?_show_hidden=1">{{ "{:,}".format(hidden_count) }} hidden table{% if hidden_count == 1 %}{% else %}s{% endif %}</a></p>
<p>... and <a href="{{ urls.database(database) }}?_show_hidden=1">{{ "{:,}".format(hidden_count) }} hidden table{% if hidden_count == 1 %}{% else %}s{% endif %}</a></p>
{% endif %}

{% if views %}
<h2 id="views">Views</h2>
<ul>
{% for view in views %}
<li><a href="{{ database_url(database) }}/{{ view.name|urlencode }}">{{ view.name }}</a>{% if view.private %} 🔒{% endif %}</li>
<li><a href="{{ urls.database(database) }}/{{ view.name|urlencode }}">{{ view.name }}</a>{% if view.private %} 🔒{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
Expand All @@ -60,13 +60,13 @@ <h2 id="views">Views</h2>
<h2 id="queries">Queries</h2>
<ul>
{% for query in queries %}
<li><a href="{{ database_url(database) }}/{{ query.name|urlencode }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a>{% if query.private %} 🔒{% endif %}</li>
<li><a href="{{ urls.query(database, query.name) }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a>{% if query.private %} 🔒{% endif %}</li>
{% endfor %}
</ul>
{% endif %}

{% if allow_download %}
<p class="download-sqlite">Download SQLite DB: <a href="{{ database_url(database) }}.db">{{ database }}.db</a> <em>{{ format_bytes(size) }}</em></p>
<p class="download-sqlite">Download SQLite DB: <a href="{{ urls.database(database) }}.db">{{ database }}.db</a> <em>{{ format_bytes(size) }}</em></p>
{% endif %}

{% include "_codemirror_foot.html" %}
Expand Down
5 changes: 2 additions & 3 deletions datasette/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h1>{{ metadata.title or "Datasette" }}{% if private %} 🔒{% endif %}</h1>
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

{% for database in databases %}
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a href="{{ database.path }}">{{ database.name }}</a>{% if database.private %} 🔒{% endif %}</h2>
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a href="{{ urls.database(database.name) }}">{{ database.name }}</a>{% if database.private %} 🔒{% endif %}</h2>
<p>
{% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.tables_count and database.hidden_tables_count %}, {% endif -%}
{% if database.hidden_tables_count -%}
Expand All @@ -21,8 +21,7 @@ <h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a
{{ "{:,}".format(database.views_count) }} view{% if database.views_count != 1 %}s{% endif %}
{% endif %}
</p>
<p>{% for table in database.tables_and_views_truncated %}<a href="{{ database.path }}/{{ table.name|quote_plus
}}"{% if table.count %} title="{{ table.count }} rows"{% endif %}>{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, <a href="{{ database.path }}">...</a>{% endif %}</p>
<p>{% for table in database.tables_and_views_truncated %}<a href="{{ urls.table(database.name, table.name) }}"{% if table.count %} title="{{ table.count }} rows"{% endif %}>{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, <a href="{{ urls.database(database.name) }}">...</a>{% endif %}</p>
{% endfor %}

{% endblock %}
6 changes: 3 additions & 3 deletions datasette/templates/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@

{% block nav %}
<p class="crumbs">
<a href="/">home</a> /
<a href="{{ database_url(database) }}">{{ database }}</a>
<a href="{{ urls.instance() }}">home</a> /
<a href="{{ urls.database(database) }}">{{ database }}</a>
</p>
{{ super() }}
{% endblock %}
Expand All @@ -32,7 +32,7 @@ <h1 style="padding-left: 10px; border-left: 10px solid #{{ database_color(databa

{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}

<form class="sql" action="{{ database_url(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_write %}post{% else %}get{% endif %}">
<form class="sql" action="{{ urls.database(database) }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="{% if canned_write %}post{% else %}get{% endif %}">
<h3>Custom SQL query{% if display_rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(display_rows|length) }} row{% if display_rows|length == 1 %}{% else %}s{% endif %}{% endif %} <span class="show-hide-sql">{% if hide_sql %}(<a href="{{ path_with_removed_args(request, {'_hide_sql': '1'}) }}">show</a>){% else %}(<a href="{{ path_with_added_args(request, {'_hide_sql': '1'}) }}">hide</a>){% endif %}</span></h3>
{% if not hide_sql %}
{% if editable and allow_execute_sql %}
Expand Down
8 changes: 4 additions & 4 deletions datasette/templates/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

{% block nav %}
<p class="crumbs">
<a href="{{ base_url }}">home</a> /
<a href="{{ database_url(database) }}">{{ database }}</a> /
<a href="{{ database_url(database) }}/{{ table|quote_plus }}">{{ table }}</a>
<a href="{{ urls.instance() }}">home</a> /
<a href="{{ urls.database(database) }}">{{ database }}</a> /
<a href="{{ urls.table(database, table) }}">{{ table }}</a>
</p>
{{ super() }}
{% endblock %}
Expand All @@ -38,7 +38,7 @@ <h2>Links from other tables</h2>
<ul>
{% for other in foreign_key_tables %}
<li>
<a href="{{ database_url(database) }}/{{ other.other_table|quote_plus }}?{{ other.other_column }}={{ ', '.join(primary_key_values) }}">
<a href="{{ urls.table(database, other.other_table) }}?{{ other.other_column }}={{ ', '.join(primary_key_values) }}">
{{ "{:,}".format(other.count) }} row{% if other.count == 1 %}{% else %}s{% endif %}</a>
from {{ other.other_column }} in {{ other.other_table }}
</li>
Expand Down
10 changes: 5 additions & 5 deletions datasette/templates/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

{% block extra_head %}
{{ super() }}
<script src="{{ base_url }}-/static/table.js" defer></script>
<script src="{{ urls.static('table.js') }}" defer></script>
<style>
@media only screen and (max-width: 576px) {
{% for column in display_columns %}
Expand All @@ -18,8 +18,8 @@

{% block nav %}
<p class="crumbs">
<a href="{{ base_url }}">home</a> /
<a href="{{ database_url(database) }}">{{ database }}</a>
<a href="{{ urls.instance() }}">home</a> /
<a href="{{ urls.database(database) }}">{{ database }}</a>
</p>
{{ super() }}
{% endblock %}
Expand All @@ -36,7 +36,7 @@ <h3>{% if filtered_table_rows_count or filtered_table_rows_count == 0 %}{{ "{:,}
</h3>
{% endif %}

<form class="filters" action="{{ database_url(database) }}/{{ table|quote_plus }}" method="get">
<form class="filters" action="{{ urls.table(database, table) }}" method="get">
{% if supports_search %}
<div class="search-row"><label for="_search">Search:</label><input id="_search" type="search" name="_search" value="{{ search }}"></div>
{% endif %}
Expand Down Expand Up @@ -107,7 +107,7 @@ <h3>{{ extra_wheres_for_ui|length }} extra where clause{% if extra_wheres_for_ui
{% endif %}

{% if query.sql and allow_execute_sql %}
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ database_url(database) }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&amp;{{ query.params|urlencode|safe }}{% endif %}">&#x270e; <span class="underlined">View and edit SQL</span></a></p>
<p><a class="not-underlined" title="{{ query.sql }}" href="{{ urls.database(database) }}?{{ {'sql': query.sql}|urlencode|safe }}{% if query.params %}&amp;{{ query.params|urlencode|safe }}{% endif %}">&#x270e; <span class="underlined">View and edit SQL</span></a></p>
{% endif %}

<p class="export-links">This data as {% for name, url in renderers.items() %}<a href="{{ url }}">{{ name }}</a>{{ ", " if not loop.last }}{% endfor %}{% if display_rows %}, <a href="{{ url_csv }}">CSV</a> (<a href="#export">advanced</a>){% endif %}</p>
Expand Down
2 changes: 2 additions & 0 deletions datasette/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
"/usr/lib/x86_64-linux-gnu/mod_spatialite.so",
"/usr/local/lib/mod_spatialite.dylib",
)
# Length of hash subset used in hashed URLs:
HASH_LENGTH = 7

# Can replace this with Column from sqlite_utils when I add that dependency
Column = namedtuple(
Expand Down
18 changes: 1 addition & 17 deletions datasette/views/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import asyncio
import csv
import itertools
import json
import re
import time
import urllib
Expand All @@ -16,27 +14,22 @@
InvalidSql,
LimitedWriter,
call_with_supported_arguments,
is_url,
path_with_added_args,
path_with_removed_args,
path_with_format,
resolve_table_and_format,
sqlite3,
to_css_class,
HASH_LENGTH,
)
from datasette.utils.asgi import (
AsgiStream,
AsgiWriter,
Forbidden,
NotFound,
Request,
Response,
)

ureg = pint.UnitRegistry()

HASH_LENGTH = 7


class DatasetteError(Exception):
def __init__(
Expand Down Expand Up @@ -99,14 +92,6 @@ async def check_permissions(self, request, permissions):
else:
raise Forbidden(action)

def database_url(self, database):
db = self.ds.databases[database]
base_url = self.ds.config("base_url")
if self.ds.config("hash_urls") and db.hash:
return "{}{}-{}".format(base_url, database, db.hash[:HASH_LENGTH])
else:
return "{}{}".format(base_url, database)

def database_color(self, database):
return "ff0000"

Expand All @@ -132,7 +117,6 @@ async def render(self, templates, request, context=None):
template_context = {
**context,
**{
"database_url": self.database_url,
"database_color": self.database_color,
"select_templates": [
"{}{}".format(
Expand Down
2 changes: 1 addition & 1 deletion datasette/views/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ async def extra_template():
pass
if allow_execute_sql and is_validated_sql and ":_" not in sql:
edit_sql_url = (
self.database_url(database)
self.ds.urls.database(database)
+ "?"
+ urlencode(
{
Expand Down
2 changes: 1 addition & 1 deletion datasette/views/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def get(self, request, as_format):
"color": db.hash[:6]
if db.hash
else hashlib.md5(name.encode("utf8")).hexdigest()[:6],
"path": self.database_url(name),
"path": self.ds.urls.database(name),
"tables_and_views_truncated": tables_and_views_truncated,
"tables_and_views_more": (len(visible_tables) + len(views))
> TRUNCATE_AT,
Expand Down
9 changes: 7 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ def test_spatialite_error_if_attempt_to_open_spatialite():
def test_spatialite_error_if_cannot_find_load_extension_spatialite():
runner = CliRunner()
result = runner.invoke(
cli, ["serve", str(pathlib.Path(__file__).parent / "spatialite.db"),
"--load-extension", "spatialite"]
cli,
[
"serve",
str(pathlib.Path(__file__).parent / "spatialite.db"),
"--load-extension",
"spatialite",
],
)
assert result.exit_code != 0
assert "Could not find SpatiaLite extension" in result.output
Expand Down

0 comments on commit 310c3a3

Please sign in to comment.