From 1c36d07dd432b9960f4f2d096739460b4fcf8877 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 17 Apr 2018 20:12:21 -0700 Subject: [PATCH] New plugin hooks: extra_css_urls() and extra_js_urls() Closes #214 --- datasette/app.py | 20 +++++++++---- datasette/hookspecs.py | 10 +++++++ docs/custom_templates.rst | 6 ++-- docs/plugins.rst | 62 +++++++++++++++++++++++++++++++++++++++ tests/fixtures.py | 13 ++++++++ tests/test_html.py | 13 ++++++++ 6 files changed, 117 insertions(+), 7 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 96176d16..df2bd2ed 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -12,6 +12,7 @@ import asyncio import os import threading import urllib.parse +import itertools import json import jinja2 import hashlib @@ -1126,7 +1127,12 @@ class Datasette: } def asset_urls(self, key): - for url_or_dict in (self.metadata.get(key) or []): + urls_or_dicts = (self.metadata.get(key) or []) + # Flatten list-of-lists from plugins: + urls_or_dicts += list( + itertools.chain.from_iterable(getattr(pm.hook, key)()) + ) + for url_or_dict in urls_or_dicts: if isinstance(url_or_dict, dict): yield { 'url': url_or_dict['url'], @@ -1297,10 +1303,14 @@ class Datasette: app.static(path, dirname) # Mount any plugin static/ directories for plugin_module in pm.get_plugins(): - if pkg_resources.resource_isdir(plugin_module.__name__, 'static'): - modpath = '/-/static-plugins/{}/'.format(plugin_module.__name__) - dirpath = pkg_resources.resource_filename(plugin_module.__name__, 'static') - app.static(modpath, dirpath) + try: + if pkg_resources.resource_isdir(plugin_module.__name__, 'static'): + modpath = '/-/static-plugins/{}/'.format(plugin_module.__name__) + dirpath = pkg_resources.resource_filename(plugin_module.__name__, 'static') + app.static(modpath, dirpath) + except ModuleNotFoundError: + # Caused by --plugins_dir= plugins + pass app.add_route( DatabaseView.as_view(self), '/' diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index 0e65a198..240b58db 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -13,3 +13,13 @@ def prepare_connection(conn): @hookspec def prepare_jinja2_environment(env): "Modify Jinja2 template environment e.g. register custom template tags" + + +@hookspec +def extra_css_urls(): + "Extra CSS URLs added by this plugin" + + +@hookspec +def extra_js_urls(): + "Extra JavaScript URLs added by this plugin" diff --git a/docs/custom_templates.rst b/docs/custom_templates.rst index 44ba98c9..1cdf588c 100644 --- a/docs/custom_templates.rst +++ b/docs/custom_templates.rst @@ -1,5 +1,7 @@ -Customizing Datasette's appearance -================================== +.. _customization: + +Customization +============= Datasette provides a number of ways of customizing the way data is displayed. diff --git a/docs/plugins.rst b/docs/plugins.rst index 981e4a3a..cdda9b78 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -169,3 +169,65 @@ example: You can now use this filter in your custom templates like so:: Table name: {{ table|uppercase }} + +extra_css_urls() +~~~~~~~~~~~~~~~~ + +Return a list of extra CSS URLs that should be included on every page. These can +take advantage of the CSS class hooks described in :ref:`customization`. + +This can be a list of URLs: + +.. code-block:: python + + from datasette import hookimpl + + @hookimpl + def extra_css_urls(): + return [ + 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css' + ] + +Or a list of dictionaries defining both a URL and an +`SRI hash `_: + +.. code-block:: python + + from datasette import hookimpl + + @hookimpl + def extra_css_urls(): + return [{ + 'url': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css', + 'sri': 'sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4', + }] + +extra_js_urls() +~~~~~~~~~~~~~~~ + +This works in the same way as ``extra_css_urls()`` but for JavaScript. You can +return either a list of URLs or a list of dictionaries: + +.. code-block:: python + + from datasette import hookimpl + + @hookimpl + def extra_js_urls(): + return [{ + 'url': 'https://code.jquery.com/jquery-3.3.1.slim.min.js', + 'sri': 'sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo', + }] + +You can also return URLs to files from your plugin's ``static/`` directory, if +you have one: + +.. code-block:: python + + from datasette import hookimpl + + @hookimpl + def extra_js_urls(): + return [ + '/-/static-plugins/your_plugin/app.js' + ] diff --git a/tests/fixtures.py b/tests/fixtures.py index 493e5272..4ba39448 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -113,6 +113,19 @@ def prepare_connection(conn): "select convert_units(100, 'm', 'ft');" return (amount * ureg(from_)).to(to_).to_tuple()[0] conn.create_function('convert_units', 3, convert_units) + + +@hookimpl +def extra_css_urls(): + return ['https://example.com/app.css'] + + +@hookimpl +def extra_js_urls(): + return [{ + 'url': 'https://example.com/app.js', + 'sri': 'SRIHASH', + }] ''' TABLES = ''' diff --git a/tests/test_html.py b/tests/test_html.py index 56e93e30..51e3d4eb 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -333,6 +333,19 @@ def test_view_html(app_client): assert expected == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')] +def test_plugin_extra_css_urls(app_client): + response = app_client.get('/', gather_request=False) + assert b'' in response.body + + +def test_plugin_extra_js_urls(app_client): + response = app_client.get('/', gather_request=False) + assert ( + b'' + in response.body + ) + + def test_index_metadata(app_client): response = app_client.get('/', gather_request=False) assert response.status == 200