Simon Willison 2017-11-12 15:17:00 -08:00
commit 26370b14d8
8 changed files with 98 additions and 33 deletions

View file

@ -17,6 +17,7 @@ from .utils import (
build_where_clauses,
CustomJSONEncoder,
escape_css_string,
escape_sqlite_table_name,
InvalidSql,
path_from_row_pks,
path_with_added_args,
@ -117,8 +118,8 @@ class BaseView(HTTPMethodView):
try:
rows = conn.execute(sql, params or {})
except Exception:
print('sql = {}, params = {}'.format(
sql, params
print('ERROR: conn={}, sql = {}, params = {}'.format(
conn, repr(sql), params
))
raise
return rows
@ -381,8 +382,8 @@ class TableView(BaseView):
if order_by:
order_by = 'order by {} '.format(order_by)
sql = 'select {} from "{}" {}{}limit {}'.format(
select, table, where_clause, order_by, self.page_size + 1,
sql = 'select {} from {} {}{}limit {}'.format(
select, escape_sqlite_table_name(table), where_clause, order_by, self.page_size + 1,
)
rows = await self.execute(name, sql, params)
@ -519,6 +520,8 @@ class Datasette:
])
)
self.jinja.add_env('escape_css_string', escape_css_string, 'filters')
self.jinja.add_env('quote_plus', lambda u: urllib.parse.quote_plus(u), 'filters')
self.jinja.add_env('escape_table_name', escape_sqlite_table_name, 'filters')
app.add_route(IndexView.as_view(self), '/<as_json:(.jsono?)?$>')
# TODO: /favicon.ico and /-/static/ deserve far-future cache expires
app.add_route(favicon, '/favicon.ico')

View file

@ -2,6 +2,18 @@
{% block title %}{{ database }}{% endblock %}
{% block extra_head %}
{% if columns %}
<style>
@media only screen and (max-width: 576px) {
{% for column in columns %}
td:nth-of-type({{ loop.index }}):before { content: "{{ column|escape_css_string }}"; }
{% endfor %}
}
</style>
{% endif %}
{% endblock %}
{% block content %}
<div class="hd"><a href="/">home</a>{% if query %} / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a>{% endif %}</div>
@ -14,13 +26,32 @@
{% endif %}
<form class="sql" action="/{{ database }}-{{ database_hash }}" method="get">
<p><textarea name="sql">{% if query and query.sql %}{{ query.sql }}{% else %}select * from {{ tables[0].name }}{% endif %}</textarea></p>
<p><textarea name="sql">{% if query and query.sql %}{{ query.sql }}{% else %}select * from {{ tables[0].name|escape_table_name }}{% endif %}</textarea></p>
<p><input type="submit" value="Run SQL"></p>
</form>
{% if rows %}
<table>
<thead>
<tr>
{% for column in columns %}<th scope="col">{{ column }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for td in row %}
<td>{{ td or "&nbsp;" }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% for table in tables %}
<div class="db-table">
<h2><a href="/{{ database }}-{{ database_hash }}/{{ table.name|urlencode }}">{{ table.name }}</a></h2>
<h2><a href="/{{ database }}-{{ database_hash }}/{{ table.name|quote_plus }}">{{ table.name }}</a></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>{{ "{:,}".format(table.table_rows) }} row{% if table.table_rows == 1 %}{% else %}s{% endif %}</p>
</div>

View file

@ -7,7 +7,7 @@
{% for database in databases %}
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.hash[:6] }}"><a href="{{ database.path }}">{{ database.name }}</a></h2>
<p>{{ "{:,}".format(database.table_rows) }} rows in {{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}</p>
<p>{% for table, count in database.tables_truncated %}<a href="{{ database.path }}/{{ table }}" title="{{ count }} rows">{{ table }}</a>{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_more %}, <a href="{{ database.path }}">...</a>{% endif %}</p>
<p>{% for table, count in database.tables_truncated %}<a href="{{ database.path }}/{{ table|quote_plus }}" title="{{ count }} rows">{{ table }}</a>{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_more %}, <a href="{{ database.path }}">...</a>{% endif %}</p>
{% endfor %}
{% endblock %}

View file

@ -13,7 +13,7 @@
{% endblock %}
{% block content %}
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a> / <a href="/{{ database }}-{{ database_hash }}/{{ table }}">{{ table }}</a></div>
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a> / <a href="/{{ database }}-{{ database_hash }}/{{ table|quote_plus }}">{{ table }}</a></div>
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ table }}: {{ ', '.join(primary_key_values) }}</a></h1>

View file

@ -36,7 +36,7 @@
<tbody>
{% for row in rows %}
<tr>
{% if not is_view %}<td><a href="/{{ database }}-{{ database_hash }}/{{ table }}/{{ row_link(row) }}">{{ row_link(row) }}</a></td>{% endif %}
{% if not is_view %}<td><a href="/{{ database }}-{{ database_hash }}/{{ table|quote_plus }}/{{ row_link(row) }}">{{ row_link(row) }}</a></td>{% endif %}
{% for td in row %}
{% if not use_rowid or (use_rowid and not loop.first) %}
<td>{{ td or "&nbsp;" }}</td>

View file

@ -119,18 +119,26 @@ def path_with_ext(request, ext):
_css_re = re.compile(r'''['"\n\\]''')
_boring_table_name_re = re.compile(r'^[a-zA-Z0-9_]+$')
def escape_css_string(s):
return _css_re.sub(lambda m: '\\{:X}'.format(ord(m.group())), s)
def escape_sqlite_table_name(s):
if _boring_table_name_re.match(s):
return s
else:
return '[{}]'.format(s)
def make_dockerfile(files):
return '''
FROM python:3
COPY . /app
WORKDIR /app
RUN pip install https://static.simonwillison.net/static/2017/datasette-0.2-py3-none-any.whl
RUN pip install https://static.simonwillison.net/static/2017/datasette-0.4-py3-none-any.whl
RUN datasette build_metadata {} --metadata metadata.json
EXPOSE 8006
CMD ["datasette", "serve", {}, "--port", "8006", "--metadata", "metadata.json"]'''.format(