From b52171db1e97e2be1ff2dc505ccf29107288b27b Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 18 Apr 2018 22:50:27 -0700 Subject: [PATCH] Plugins can now bundle custom templates, closes #224 Refs #14 --- datasette/app.py | 27 +++++++++++++++++---------- datasette/utils.py | 4 ++++ docs/plugins.rst | 15 +++++++++++++++ tests/test_api.py | 6 +++++- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index cc3a9c08..79ce012e 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1303,16 +1303,22 @@ class Datasette: def app(self): app = Sanic(__name__) default_templates = str(app_root / 'datasette' / 'templates') + template_paths = [] if self.template_dir: - template_loader = ChoiceLoader([ - FileSystemLoader([self.template_dir, default_templates]), - # Support {% extends "default:table.html" %}: - PrefixLoader({ - 'default': FileSystemLoader(default_templates), - }, delimiter=':') - ]) - else: - template_loader = FileSystemLoader(default_templates) + template_paths.append(self.template_dir) + template_paths.extend([ + plugin['templates_path'] + for plugin in get_plugins(pm) + if plugin['templates_path'] + ]) + template_paths.append(default_templates) + template_loader = ChoiceLoader([ + FileSystemLoader(template_paths), + # Support {% extends "default:table.html" %}: + PrefixLoader({ + 'default': FileSystemLoader(default_templates), + }, delimiter=':') + ]) self.jinja_env = Environment( loader=template_loader, autoescape=True, @@ -1344,7 +1350,8 @@ class Datasette: app.add_route( JsonDataView.as_view(self, 'plugins.json', lambda: [{ 'name': p['name'], - 'static': p['static_path'] is not None + 'static': p['static_path'] is not None, + 'templates': p['templates_path'] is not None, } for p in get_plugins(pm)]), '/-/plugins' ) diff --git a/datasette/utils.py b/datasette/utils.py index 3ef045fb..863079bf 100644 --- a/datasette/utils.py +++ b/datasette/utils.py @@ -690,14 +690,18 @@ def get_plugins(pm): plugins = [] for plugin in pm.get_plugins(): static_path = None + templates_path = None try: if pkg_resources.resource_isdir(plugin.__name__, 'static'): static_path = pkg_resources.resource_filename(plugin.__name__, 'static') + if pkg_resources.resource_isdir(plugin.__name__, 'templates'): + templates_path = pkg_resources.resource_filename(plugin.__name__, 'templates') except (KeyError, ImportError): # Caused by --plugins_dir= plugins - KeyError/ImportError thrown in Py3.5 pass plugins.append({ 'name': plugin.__name__, 'static_path': static_path, + 'templates_path': templates_path, }) return plugins diff --git a/docs/plugins.rst b/docs/plugins.rst index 40e73436..b83527ff 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -132,6 +132,21 @@ configure itself to serve those static assets from the following path:: See `the datasette-plugin-demos repository `_ for an example of how to create a package that includes a static folder. +Custom templates +---------------- + +If your plugin has a ``templates/`` directory, Datasette will attempt to load +templates from that directory before it uses its own default templates. + +The priority order for template loading is: + +* templates from the ``--template-dir`` argument, if specified +* templates from the ``templates/`` directory in any installed plugins +* default templates that ship with Datasette + +See :ref:`customization` for more details on how to write custom templates, +including which filenames to use to customize which parts of the Datasette UI. + Plugin hooks ------------ diff --git a/tests/test_api.py b/tests/test_api.py index e1db8c46..4f1ed6f2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -656,4 +656,8 @@ def test_plugins_json(app_client): # This will include any plugins that have been installed into the # current virtual environment, so we only check for the presence of # the one we know will definitely be There - assert {'name': 'my_plugin.py', 'static': False} in response.json + assert { + 'name': 'my_plugin.py', + 'static': False, + 'templates': False + } in response.json