mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Ability to over-ride templates for individual tables/databases
It is now possible to over-ride templates on a per-database / per-row or per-
table basis.
When you access e.g. /mydatabase/mytable Datasette will look for the following:
- table-mydatabase-mytable.html
- table.html
If you provided a --template-dir argument to datasette serve it will look in
that directory first.
The lookup rules are as follows:
Index page (/):
index.html
Database page (/mydatabase):
database-mydatabase.html
database.html
Table page (/mydatabase/mytable):
table-mydatabase-mytable.html
table.html
Row page (/mydatabase/mytable/id):
row-mydatabase-mytable.html
row.html
If a table name has spaces or other unexpected characters in it, the template
filename will follow the same rules as our custom <body> CSS classes
introduced in 8ab3a169d4 - for example, a table called "Food Trucks"
will attempt to load the following templates:
table-mydatabase-Food-Trucks-399138.html
table.html
It is possible to extend the default templates using Jinja template
inheritance. If you want to customize EVERY row template with some additional
content you can do so by creating a row.html template like this:
{% extends "default:row.html" %}
{% block content %}
<h1>EXTRA HTML AT THE TOP OF THE CONTENT BLOCK</h1>
<p>This line renders the original block:</p>
{{ super() }}
{% endblock %}
Closes #12, refs #153
This commit is contained in:
parent
7ff51598c4
commit
3cd06729f4
1 changed files with 32 additions and 23 deletions
|
|
@ -3,7 +3,7 @@ from sanic import response
|
||||||
from sanic.exceptions import NotFound
|
from sanic.exceptions import NotFound
|
||||||
from sanic.views import HTTPMethodView
|
from sanic.views import HTTPMethodView
|
||||||
from sanic.request import RequestParameters
|
from sanic.request import RequestParameters
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader
|
||||||
import re
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -43,15 +43,13 @@ connections = threading.local()
|
||||||
|
|
||||||
|
|
||||||
class RenderMixin(HTTPMethodView):
|
class RenderMixin(HTTPMethodView):
|
||||||
def render(self, template, **context):
|
def render(self, templates, **context):
|
||||||
return response.html(
|
return response.html(
|
||||||
self.jinja_env.get_template(template).render(**context)
|
self.jinja_env.select_template(templates).render(**context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseView(RenderMixin):
|
class BaseView(RenderMixin):
|
||||||
template = None
|
|
||||||
|
|
||||||
def __init__(self, datasette):
|
def __init__(self, datasette):
|
||||||
self.ds = datasette
|
self.ds = datasette
|
||||||
self.files = datasette.files
|
self.files = datasette.files
|
||||||
|
|
@ -166,6 +164,9 @@ class BaseView(RenderMixin):
|
||||||
self.executor, sql_operation_in_thread
|
self.executor, sql_operation_in_thread
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_templates(self, database, table=None):
|
||||||
|
assert NotImplemented
|
||||||
|
|
||||||
async def get(self, request, db_name, **kwargs):
|
async def get(self, request, db_name, **kwargs):
|
||||||
name, hash, should_redirect = self.resolve_db_name(db_name, **kwargs)
|
name, hash, should_redirect = self.resolve_db_name(db_name, **kwargs)
|
||||||
if should_redirect:
|
if should_redirect:
|
||||||
|
|
@ -179,8 +180,8 @@ class BaseView(RenderMixin):
|
||||||
as_json = False
|
as_json = False
|
||||||
extra_template_data = {}
|
extra_template_data = {}
|
||||||
start = time.time()
|
start = time.time()
|
||||||
template = self.template
|
|
||||||
status_code = 200
|
status_code = 200
|
||||||
|
templates = []
|
||||||
try:
|
try:
|
||||||
response_or_template_contexts = await self.data(
|
response_or_template_contexts = await self.data(
|
||||||
request, name, hash, **kwargs
|
request, name, hash, **kwargs
|
||||||
|
|
@ -188,7 +189,7 @@ class BaseView(RenderMixin):
|
||||||
if isinstance(response_or_template_contexts, response.HTTPResponse):
|
if isinstance(response_or_template_contexts, response.HTTPResponse):
|
||||||
return response_or_template_contexts
|
return response_or_template_contexts
|
||||||
else:
|
else:
|
||||||
data, extra_template_data = response_or_template_contexts
|
data, extra_template_data, templates = response_or_template_contexts
|
||||||
except (sqlite3.OperationalError, InvalidSql) as e:
|
except (sqlite3.OperationalError, InvalidSql) as e:
|
||||||
data = {
|
data = {
|
||||||
'ok': False,
|
'ok': False,
|
||||||
|
|
@ -196,8 +197,8 @@ class BaseView(RenderMixin):
|
||||||
'database': name,
|
'database': name,
|
||||||
'database_hash': hash,
|
'database_hash': hash,
|
||||||
}
|
}
|
||||||
template = 'error.html'
|
|
||||||
status_code = 400
|
status_code = 400
|
||||||
|
templates = ['error.html']
|
||||||
end = time.time()
|
end = time.time()
|
||||||
data['query_ms'] = (end - start) * 1000
|
data['query_ms'] = (end - start) * 1000
|
||||||
for key in ('source', 'source_url', 'license', 'license_url'):
|
for key in ('source', 'source_url', 'license', 'license_url'):
|
||||||
|
|
@ -246,7 +247,7 @@ class BaseView(RenderMixin):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r = self.render(
|
r = self.render(
|
||||||
template,
|
templates,
|
||||||
**context,
|
**context,
|
||||||
)
|
)
|
||||||
r.status = status_code
|
r.status = status_code
|
||||||
|
|
@ -300,7 +301,7 @@ class IndexView(RenderMixin):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return self.render(
|
return self.render(
|
||||||
'index.html',
|
['index.html'],
|
||||||
databases=databases,
|
databases=databases,
|
||||||
metadata=self.ds.metadata,
|
metadata=self.ds.metadata,
|
||||||
datasette_version=__version__,
|
datasette_version=__version__,
|
||||||
|
|
@ -314,7 +315,6 @@ async def favicon(request):
|
||||||
|
|
||||||
|
|
||||||
class DatabaseView(BaseView):
|
class DatabaseView(BaseView):
|
||||||
template = 'database.html'
|
|
||||||
re_named_parameter = re.compile(':([a-zA-Z0-9_]+)')
|
re_named_parameter = re.compile(':([a-zA-Z0-9_]+)')
|
||||||
|
|
||||||
async def data(self, request, name, hash):
|
async def data(self, request, name, hash):
|
||||||
|
|
@ -331,7 +331,7 @@ class DatabaseView(BaseView):
|
||||||
}, {
|
}, {
|
||||||
'database_hash': hash,
|
'database_hash': hash,
|
||||||
'show_hidden': request.args.get('_show_hidden'),
|
'show_hidden': request.args.get('_show_hidden'),
|
||||||
}
|
}, ('database-{}.html'.format(to_css_class(name)), 'database.html')
|
||||||
|
|
||||||
async def custom_sql(self, request, name, hash):
|
async def custom_sql(self, request, name, hash):
|
||||||
params = request.raw_args
|
params = request.raw_args
|
||||||
|
|
@ -370,7 +370,7 @@ class DatabaseView(BaseView):
|
||||||
'database_hash': hash,
|
'database_hash': hash,
|
||||||
'custom_sql': True,
|
'custom_sql': True,
|
||||||
'named_parameter_values': named_parameter_values,
|
'named_parameter_values': named_parameter_values,
|
||||||
}
|
}, ('database-{}.html'.format(to_css_class(name)), 'database.html')
|
||||||
|
|
||||||
|
|
||||||
class DatabaseDownload(BaseView):
|
class DatabaseDownload(BaseView):
|
||||||
|
|
@ -464,8 +464,6 @@ class RowTableShared(BaseView):
|
||||||
|
|
||||||
|
|
||||||
class TableView(RowTableShared):
|
class TableView(RowTableShared):
|
||||||
template = 'table.html'
|
|
||||||
|
|
||||||
async def data(self, request, name, hash, table):
|
async def data(self, request, name, hash, table):
|
||||||
table = urllib.parse.unquote_plus(table)
|
table = urllib.parse.unquote_plus(table)
|
||||||
pks = await self.pks_for_table(name, table)
|
pks = await self.pks_for_table(name, table)
|
||||||
|
|
@ -681,12 +679,13 @@ class TableView(RowTableShared):
|
||||||
},
|
},
|
||||||
'next': next_value and str(next_value) or None,
|
'next': next_value and str(next_value) or None,
|
||||||
'next_url': next_url,
|
'next_url': next_url,
|
||||||
}, extra_template
|
}, extra_template, (
|
||||||
|
'table-{}-{}.html'.format(to_css_class(name), to_css_class(table)),
|
||||||
|
'table.html'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RowView(RowTableShared):
|
class RowView(RowTableShared):
|
||||||
template = 'row.html'
|
|
||||||
|
|
||||||
async def data(self, request, name, hash, table, pk_path):
|
async def data(self, request, name, hash, table, pk_path):
|
||||||
table = urllib.parse.unquote_plus(table)
|
table = urllib.parse.unquote_plus(table)
|
||||||
pk_values = compound_pks_from_path(pk_path)
|
pk_values = compound_pks_from_path(pk_path)
|
||||||
|
|
@ -733,7 +732,10 @@ class RowView(RowTableShared):
|
||||||
if 'foreign_key_tables' in (request.raw_args.get('_extras') or '').split(','):
|
if 'foreign_key_tables' in (request.raw_args.get('_extras') or '').split(','):
|
||||||
data['foreign_key_tables'] = await self.foreign_key_tables(name, table, pk_values)
|
data['foreign_key_tables'] = await self.foreign_key_tables(name, table, pk_values)
|
||||||
|
|
||||||
return data, template_data
|
return data, template_data, (
|
||||||
|
'row-{}-{}.html'.format(to_css_class(name), to_css_class(table)),
|
||||||
|
'row.html'
|
||||||
|
)
|
||||||
|
|
||||||
async def foreign_key_tables(self, name, table, pk_values):
|
async def foreign_key_tables(self, name, table, pk_values):
|
||||||
if len(pk_values) != 1:
|
if len(pk_values) != 1:
|
||||||
|
|
@ -893,12 +895,19 @@ class Datasette:
|
||||||
|
|
||||||
def app(self):
|
def app(self):
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
template_paths = []
|
default_templates = str(app_root / 'datasette' / 'templates')
|
||||||
if self.template_dir:
|
if self.template_dir:
|
||||||
template_paths.append(self.template_dir)
|
template_loader = ChoiceLoader([
|
||||||
template_paths.append(str(app_root / 'datasette' / 'templates'))
|
FileSystemLoader([self.template_dir, default_templates]),
|
||||||
|
# Support {% extends "default:table.html" %}:
|
||||||
|
PrefixLoader({
|
||||||
|
'default': FileSystemLoader(default_templates),
|
||||||
|
}, delimiter=':')
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
template_loader = FileSystemLoader(default_templates)
|
||||||
self.jinja_env = Environment(
|
self.jinja_env = Environment(
|
||||||
loader=FileSystemLoader(template_paths),
|
loader=template_loader,
|
||||||
autoescape=True,
|
autoescape=True,
|
||||||
)
|
)
|
||||||
self.jinja_env.filters['escape_css_string'] = escape_css_string
|
self.jinja_env.filters['escape_css_string'] = escape_css_string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue