From 699be7dea9499355d576c16e330b85b90e1aca2a Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 14 Sep 2020 10:39:13 -0700 Subject: [PATCH] raise_404() function for use in custom templates, closes #964 --- datasette/app.py | 24 ++++++++--- docs/custom_templates.rst | 85 +++++++++++++++++++++++++++++--------- tests/test_custom_pages.py | 15 ++++++- 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/datasette/app.py b/datasette/app.py index 17ae28ac..b17517ba 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1053,19 +1053,27 @@ class DatasetteRouter: headers["Location"] = location return "" + def raise_404(message=""): + raise NotFoundExplicit(message) + context.update( { "custom_header": custom_header, "custom_status": custom_status, "custom_redirect": custom_redirect, + "raise_404": raise_404, } ) - body = await self.ds.render_template( - template, - context, - request=request, - view_name="page", - ) + try: + body = await self.ds.render_template( + template, + context, + request=request, + view_name="page", + ) + except NotFoundExplicit as e: + await self.handle_500(request, send, e) + return # Pull content-type out into separate parameter content_type = "text/html; charset=utf-8" matches = [k for k in headers if k.lower() == "content-type"] @@ -1199,3 +1207,7 @@ def route_pattern_from_filepath(filepath): else: re_bits.append(re.escape(bit)) return re.compile("".join(re_bits)) + + +class NotFoundExplicit(NotFound): + pass diff --git a/docs/custom_templates.rst b/docs/custom_templates.rst index 32dd6657..382fd8f3 100644 --- a/docs/custom_templates.rst +++ b/docs/custom_templates.rst @@ -1,7 +1,7 @@ .. _customization: -Customization -============= +Custom pages and templates +========================== Datasette provides a number of ways of customizing the way data is displayed. @@ -12,7 +12,9 @@ When you launch Datasette, you can specify a custom metadata file like this:: datasette mydb.db --metadata metadata.json -Your ``metadata.json`` file can include links that look like this:: +Your ``metadata.json`` file can include links that look like this: + +.. code-block:: json { "extra_css_urls": [ @@ -25,7 +27,9 @@ Your ``metadata.json`` file can include links that look like this:: The extra CSS and JavaScript files will be linked in the ```` of every page. -You can also specify a SRI (subresource integrity hash) for these assets:: +You can also specify a SRI (subresource integrity hash) for these assets: + +.. code-block:: json { "extra_css_urls": [ @@ -51,27 +55,39 @@ CSS classes on the Every default template includes CSS classes in the body designed to support custom styling. -The index template (the top level page at ``/``) gets this:: +The index template (the top level page at ``/``) gets this: + +.. code-block:: html -The database template (``/dbname``) gets this:: +The database template (``/dbname``) gets this: + +.. code-block:: html -The custom SQL template (``/dbname?sql=...``) gets this:: +The custom SQL template (``/dbname?sql=...``) gets this: + +.. code-block:: html -A canned query template (``/dbname/queryname``) gets this:: +A canned query template (``/dbname/queryname``) gets this: + +.. code-block:: html -The table template (``/dbname/tablename``) gets:: +The table template (``/dbname/tablename``) gets: + +.. code-block:: html -The row template (``/dbname/tablename/rowid``) gets:: +The row template (``/dbname/tablename/rowid``) gets: + +.. code-block:: html @@ -92,7 +108,9 @@ Some examples:: "no $ characters" => "no--characters-59e024" ```` and ```` elements also get custom CSS classes reflecting the -database column they are representing, for example:: +database column they are representing, for example: + +.. code-block:: html @@ -131,7 +149,9 @@ The following URLs will now serve the content from those CSS and JS files:: http://localhost:8001/static/styles.css http://localhost:8001/static/app.js -You can reference those files from ``metadata.json`` like so:: +You can reference those files from ``metadata.json`` like so: + +.. code-block:: json { "extra_css_urls": [ @@ -229,7 +249,9 @@ used that - but if that template had not been found, it would have tried for 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:: +content you can do so by creating a ``row.html`` template like this: + +.. code-block:: jinja {% extends "default:row.html" %} @@ -258,7 +280,9 @@ of a specific column. If you want to output the rendered HTML version of a column, including any links to foreign keys, you can use ``{{ row.display("column_name") }}``. -Here is an example of a custom ``_table.html`` template:: +Here is an example of a custom ``_table.html`` template: + +.. code-block:: jinja {% for row in display_rows %}
@@ -294,7 +318,7 @@ For example, to capture any request to a URL matching ``/about/*``, you would cr A hit to ``/about/news`` would render that template and pass in a variable called ``slug`` with a value of ``"news"``. -If you use this mechanism don't forget to return a 404 status code if the page should not be considered a valid page. You can do this using ``{{ custom_status(404) }}`` described below. +If you use this mechanism don't forget to return a 404 if the referenced content could not be found. You can do this using ``{{ raise_404() }}`` described below. Templates defined using custom page routes work particularly well with the ``sql()`` template function from `datasette-template-sql `__ or the ``graphql()`` template function from `datasette-graphql `__. @@ -305,7 +329,9 @@ Custom headers and status codes Custom pages default to being served with a content-type of ``text/html; charset=utf-8`` and a ``200`` status code. You can change these by calling a custom function from within your template. -For example, to serve a custom page with a ``418 I'm a teapot`` HTTP status code, create a file in ``pages/teapot.html`` containing the following:: +For example, to serve a custom page with a ``418 I'm a teapot`` HTTP status code, create a file in ``pages/teapot.html`` containing the following: + +.. code-block:: jinja {{ custom_status(418) }} @@ -315,7 +341,9 @@ For example, to serve a custom page with a ``418 I'm a teapot`` HTTP status code -To serve a custom HTTP header, add a ``custom_header(name, value)`` function call. For example:: +To serve a custom HTTP header, add a ``custom_header(name, value)`` function call. For example: + +.. code-block:: jinja {{ custom_status(418) }} {{ custom_header("x-teapot", "I am") }} @@ -335,17 +363,36 @@ You can verify this is working using ``curl`` like this:: x-teapot: I am content-type: text/html; charset=utf-8 +.. _custom_pages_404: + +Returning 404s +~~~~~~~~~~~~~~ + +To indicate that content could not be found and display the default 404 page you can use the ``raise_404(message)`` function: + +.. code-block:: jinja + + {% if not rows %} + {{ raise_404("Content not found") }} + {% endif %} + +If you call ``raise_404()`` the other content in your template will be ignored. + .. _custom_pages_redirects: Custom redirects ~~~~~~~~~~~~~~~~ -You can use the ``custom_redirect(location)`` function to redirect users to another page, for example in a file called ``pages/datasette.html``:: +You can use the ``custom_redirect(location)`` function to redirect users to another page, for example in a file called ``pages/datasette.html``: + +.. code-block:: jinja {{ custom_redirect("https://github.com/simonw/datasette") }} Now requests to ``http://localhost:8001/datasette`` will result in a redirect. -These redirects are served with a ``301 Found`` status code by default. You can send a ``301 Moved Permanently`` code by passing ``301`` as the second argument to the function:: +These redirects are served with a ``301 Found`` status code by default. You can send a ``301 Moved Permanently`` code by passing ``301`` as the second argument to the function: + +.. code-block:: jinja {{ custom_redirect("https://github.com/simonw/datasette", 301) }} diff --git a/tests/test_custom_pages.py b/tests/test_custom_pages.py index dc3be844..89e59a75 100644 --- a/tests/test_custom_pages.py +++ b/tests/test_custom_pages.py @@ -26,7 +26,11 @@ def custom_pages_client(tmp_path_factory): '{{ custom_redirect("/example", 301) }}', "utf-8" ) (pages_dir / "route_{name}.html").write_text( - "

Hello from {{ name }}

", "utf-8" + """ + {% if name == "OhNo" %}{{ raise_404("Oh no") }}{% endif %} +

Hello from {{ name }}

+ """, + "utf-8", ) nested_dir = pages_dir / "nested" nested_dir.mkdir() @@ -91,4 +95,11 @@ def test_redirect2(custom_pages_client): def test_custom_route_pattern(custom_pages_client): response = custom_pages_client.get("/route_Sally") assert response.status == 200 - assert response.text == "

Hello from Sally

" + assert response.text.strip() == "

Hello from Sally

" + + +def test_custom_route_pattern_404(custom_pages_client): + response = custom_pages_client.get("/route_OhNo") + assert response.status == 404 + assert "

Error 404

" in response.text + assert ">Oh no