mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Better mechanism for handling errors; 404s for missing table/database
New error mechanism closes #193 404s for missing tables/databesse closes #184 Makes pull request #202 unnecessary.
This commit is contained in:
parent
ad6142b67c
commit
9f28bbe43d
6 changed files with 101 additions and 42 deletions
|
|
@ -47,7 +47,11 @@ connections = threading.local()
|
|||
|
||||
|
||||
class DatasetteError(Exception):
|
||||
pass
|
||||
def __init__(self, message, title=None, error_dict=None, status=500, template=None):
|
||||
self.message = message
|
||||
self.title = title
|
||||
self.error_dict = error_dict or {}
|
||||
self.status = status
|
||||
|
||||
|
||||
class RenderMixin(HTTPMethodView):
|
||||
|
|
@ -200,14 +204,11 @@ class BaseView(RenderMixin):
|
|||
else:
|
||||
data, extra_template_data, templates = response_or_template_contexts
|
||||
except (sqlite3.OperationalError, InvalidSql, DatasetteError) as e:
|
||||
data = {
|
||||
'ok': False,
|
||||
'error': str(e),
|
||||
'database': name,
|
||||
'database_hash': hash,
|
||||
}
|
||||
status_code = 400
|
||||
templates = ['error.html']
|
||||
raise DatasetteError(str(e), title='Invalid SQL', status=400)
|
||||
except (sqlite3.OperationalError) as e:
|
||||
raise DatasetteError(str(e))
|
||||
except DatasetteError:
|
||||
raise
|
||||
end = time.time()
|
||||
data['query_ms'] = (end - start) * 1000
|
||||
for key in ('source', 'source_url', 'license', 'license_url'):
|
||||
|
|
@ -554,19 +555,28 @@ class TableView(RowTableShared):
|
|||
canned_query = self.ds.get_canned_query(name, table)
|
||||
if canned_query is not None:
|
||||
return await self.custom_sql(request, name, hash, canned_query['sql'], editable=False, canned_query=table)
|
||||
is_view = bool(list(await self.execute(name, "SELECT count(*) from sqlite_master WHERE type = 'view' and name=:n", {
|
||||
'n': table,
|
||||
}))[0][0])
|
||||
is_view = bool(list(await self.execute(
|
||||
name,
|
||||
"SELECT count(*) from sqlite_master WHERE type = 'view' and name=:n",
|
||||
{'n': table}
|
||||
))[0][0])
|
||||
view_definition = None
|
||||
table_definition = None
|
||||
if is_view:
|
||||
view_definition = list(await self.execute(name, 'select sql from sqlite_master where name = :n and type="view"', {
|
||||
'n': table,
|
||||
}))[0][0]
|
||||
view_definition = list(await self.execute(
|
||||
name,
|
||||
'select sql from sqlite_master where name = :n and type="view"',
|
||||
{'n': table}
|
||||
))[0][0]
|
||||
else:
|
||||
table_definition = list(await self.execute(name, 'select sql from sqlite_master where name = :n and type="table"', {
|
||||
'n': table,
|
||||
}))[0][0]
|
||||
table_definition_rows = list(await self.execute(
|
||||
name,
|
||||
'select sql from sqlite_master where name = :n and type="table"',
|
||||
{'n': table}
|
||||
))
|
||||
if not table_definition_rows:
|
||||
raise NotFound('Table not found: {}'.format(table))
|
||||
table_definition = table_definition_rows[0][0]
|
||||
info = self.ds.inspect()
|
||||
table_info = info[name]['tables'].get(table) or {}
|
||||
pks = table_info.get('primary_keys') or []
|
||||
|
|
@ -1199,4 +1209,36 @@ class Datasette:
|
|||
RowView.as_view(self),
|
||||
'/<db_name:[^/]+>/<table:[^/]+?>/<pk_path:[^/]+?><as_json:(\.jsono?)?$>'
|
||||
)
|
||||
|
||||
@app.exception(Exception)
|
||||
def on_exception(request, exception):
|
||||
title = None
|
||||
if isinstance(exception, NotFound):
|
||||
status = 404
|
||||
info = {}
|
||||
message = exception.args[0]
|
||||
elif isinstance(exception, DatasetteError):
|
||||
status = exception.status
|
||||
info = exception.error_dict
|
||||
message = exception.message
|
||||
title = exception.title
|
||||
else:
|
||||
status = 500
|
||||
info = {}
|
||||
message = str(exception)
|
||||
templates = ['500.html']
|
||||
if status != 500:
|
||||
templates = ['{}.html'.format(status)] + templates
|
||||
info.update({
|
||||
'ok': False,
|
||||
'error': message,
|
||||
'status': status,
|
||||
'title': title,
|
||||
})
|
||||
if (request.path.split('?')[0].endswith('.json')):
|
||||
return response.json(info, status=status)
|
||||
else:
|
||||
template = self.jinja_env.select_template(templates)
|
||||
return response.html(template.render(info), status=status)
|
||||
|
||||
return app
|
||||
|
|
|
|||
12
datasette/templates/500.html
Normal file
12
datasette/templates/500.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if title %}{{ title }}{% else %}Error {{ status }}{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hd"><a href="/">home</a></div>
|
||||
|
||||
<h1>{% if title %}{{ title }}{% else %}Error {{ status }}{% endif %}</h1>
|
||||
|
||||
<div style="padding: 1em; margin: 1em 0; border: 3px solid red;">{{ error }}</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -20,17 +20,19 @@
|
|||
<div class="ft">
|
||||
Powered by <a href="https://github.com/simonw/datasette" title="Datasette v{{ datasette_version }}">Datasette</a>
|
||||
{% if query_ms %}· Query took {{ query_ms|round(3) }}ms{% endif %}
|
||||
{% if metadata.license or metadata.license_url %}· Data license:
|
||||
{% if metadata.license_url %}
|
||||
<a href="{{ metadata.license_url }}">{{ metadata.license or metadata.license_url }}</a>
|
||||
{% else %}
|
||||
{{ metadata.license }}
|
||||
{% if metadata %}
|
||||
{% if metadata.license or metadata.license_url %}· Data license:
|
||||
{% if metadata.license_url %}
|
||||
<a href="{{ metadata.license_url }}">{{ metadata.license or metadata.license_url }}</a>
|
||||
{% else %}
|
||||
{{ metadata.license }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if metadata.source or metadata.source_url %}·
|
||||
Data source: {% if metadata.source_url %}
|
||||
<a href="{{ metadata.source_url }}">
|
||||
{% endif %}{{ metadata.source or metadata.source_url }}{% if metadata.source_url %}</a>{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if metadata.source or metadata.source_url %}·
|
||||
Data source: {% if metadata.source_url %}
|
||||
<a href="{{ metadata.source_url }}">
|
||||
{% endif %}{{ metadata.source or metadata.source_url }}{% if metadata.source_url %}</a>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if select_templates %}<!-- Templates considered: {{ select_templates|join(", ") }} -->{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ database }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a></div>
|
||||
|
||||
<h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash and database_hash[:6] }}">{{ database }}</h1>
|
||||
|
||||
{% if error %}
|
||||
<div style="padding: 1em; margin: 1em 0; border: 3px solid red;">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -208,6 +208,17 @@ def test_table_json(app_client):
|
|||
}]
|
||||
|
||||
|
||||
def test_table_not_exists_json(app_client):
|
||||
assert {
|
||||
'ok': False,
|
||||
'error': 'Table not found: blah',
|
||||
'status': 404,
|
||||
'title': None,
|
||||
} == app_client.get(
|
||||
'/test_tables/blah.json', gather_request=False
|
||||
).json
|
||||
|
||||
|
||||
def test_jsono_redirects_to_shape_objects(app_client):
|
||||
response_1 = app_client.get(
|
||||
'/test_tables/simple_primary_key.jsono',
|
||||
|
|
|
|||
|
|
@ -210,6 +210,12 @@ def test_row_html_simple_primary_key(app_client):
|
|||
] == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')]
|
||||
|
||||
|
||||
def test_table_not_exists(app_client):
|
||||
assert 'Table not found: blah' in app_client.get(
|
||||
'/test_tables/blah', gather_request=False
|
||||
).body.decode('utf8')
|
||||
|
||||
|
||||
def test_table_html_no_primary_key(app_client):
|
||||
response = app_client.get('/test_tables/no_primary_key', gather_request=False)
|
||||
table = Soup(response.body, 'html.parser').find('table')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue