mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Optional path parameters for custom pages, closes #944
This commit is contained in:
parent
ea340cf320
commit
cc77fcd133
3 changed files with 72 additions and 3 deletions
|
|
@ -942,6 +942,16 @@ class DatasetteRouter:
|
||||||
((re.compile(pattern) if isinstance(pattern, str) else pattern), view)
|
((re.compile(pattern) if isinstance(pattern, str) else pattern), view)
|
||||||
for pattern, view in routes
|
for pattern, view in routes
|
||||||
]
|
]
|
||||||
|
# Build a list of pages/blah/{name}.html matching expressions
|
||||||
|
pattern_templates = [
|
||||||
|
filepath
|
||||||
|
for filepath in self.ds.jinja_env.list_templates()
|
||||||
|
if "{" in filepath and filepath.startswith("pages/")
|
||||||
|
]
|
||||||
|
self.page_routes = [
|
||||||
|
(route_pattern_from_filepath(filepath[len("pages/") :]), filepath)
|
||||||
|
for filepath in pattern_templates
|
||||||
|
]
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
async def __call__(self, scope, receive, send):
|
||||||
# Because we care about "foo/bar" v.s. "foo%2Fbar" we decode raw_path ourselves
|
# Because we care about "foo/bar" v.s. "foo%2Fbar" we decode raw_path ourselves
|
||||||
|
|
@ -1002,6 +1012,7 @@ class DatasetteRouter:
|
||||||
async def handle_404(self, request, send, exception=None):
|
async def handle_404(self, request, send, exception=None):
|
||||||
# If URL has a trailing slash, redirect to URL without it
|
# If URL has a trailing slash, redirect to URL without it
|
||||||
path = request.scope.get("raw_path", request.scope["path"].encode("utf8"))
|
path = request.scope.get("raw_path", request.scope["path"].encode("utf8"))
|
||||||
|
context = {}
|
||||||
if path.endswith(b"/"):
|
if path.endswith(b"/"):
|
||||||
path = path.rstrip(b"/")
|
path = path.rstrip(b"/")
|
||||||
if request.scope["query_string"]:
|
if request.scope["query_string"]:
|
||||||
|
|
@ -1016,6 +1027,15 @@ class DatasetteRouter:
|
||||||
template = self.ds.jinja_env.select_template([template_path])
|
template = self.ds.jinja_env.select_template([template_path])
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
template = None
|
template = None
|
||||||
|
if template is None:
|
||||||
|
# Try for a pages/blah/{name}.html template match
|
||||||
|
for regex, wildcard_template in self.page_routes:
|
||||||
|
match = regex.match(request.scope["path"])
|
||||||
|
if match is not None:
|
||||||
|
context.update(match.groupdict())
|
||||||
|
template = wildcard_template
|
||||||
|
break
|
||||||
|
|
||||||
if template:
|
if template:
|
||||||
headers = {}
|
headers = {}
|
||||||
status = [200]
|
status = [200]
|
||||||
|
|
@ -1033,13 +1053,16 @@ class DatasetteRouter:
|
||||||
headers["Location"] = location
|
headers["Location"] = location
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
body = await self.ds.render_template(
|
context.update(
|
||||||
template,
|
|
||||||
{
|
{
|
||||||
"custom_header": custom_header,
|
"custom_header": custom_header,
|
||||||
"custom_status": custom_status,
|
"custom_status": custom_status,
|
||||||
"custom_redirect": custom_redirect,
|
"custom_redirect": custom_redirect,
|
||||||
},
|
}
|
||||||
|
)
|
||||||
|
body = await self.ds.render_template(
|
||||||
|
template,
|
||||||
|
context,
|
||||||
request=request,
|
request=request,
|
||||||
view_name="page",
|
view_name="page",
|
||||||
)
|
)
|
||||||
|
|
@ -1160,3 +1183,19 @@ def wrap_view(view_fn, datasette):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return async_view_fn
|
return async_view_fn
|
||||||
|
|
||||||
|
|
||||||
|
_curly_re = re.compile("(\{.*?\})")
|
||||||
|
|
||||||
|
|
||||||
|
def route_pattern_from_filepath(filepath):
|
||||||
|
# Drop the ".html" suffix
|
||||||
|
if filepath.endswith(".html"):
|
||||||
|
filepath = filepath[: -len(".html")]
|
||||||
|
re_bits = ["/"]
|
||||||
|
for bit in _curly_re.split(filepath):
|
||||||
|
if _curly_re.match(bit):
|
||||||
|
re_bits.append("(?P<{}>[^/]*)".format(bit[1:-1]))
|
||||||
|
else:
|
||||||
|
re_bits.append(re.escape(bit))
|
||||||
|
return re.compile("".join(re_bits))
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,25 @@ For example, to add a custom page that is served at ``http://localhost/about`` y
|
||||||
|
|
||||||
You can nest directories within pages to create a nested structure. To create a ``http://localhost:8001/about/map`` page you would create ``templates/pages/about/map.html``.
|
You can nest directories within pages to create a nested structure. To create a ``http://localhost:8001/about/map`` page you would create ``templates/pages/about/map.html``.
|
||||||
|
|
||||||
|
.. _custom_pages_parameters:
|
||||||
|
|
||||||
|
Path parameters for pages
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can define custom pages that match multiple paths by creating files with ``{variable}`` definitions in their filenames.
|
||||||
|
|
||||||
|
For example, to capture any request to a URL matching ``/about/*``, you would create a template in the following location::
|
||||||
|
|
||||||
|
templates/pages/about/{slug}.html
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Templates defined using custom page routes work particularly well with the ``sql()`` template function from `datasette-template-sql <https://github.com/simonw/datasette-template-sql>`__ or the ``graphql()`` template function from `datasette-graphql <https://github.com/simonw/datasette-graphql#the-graphql-template-function>`__.
|
||||||
|
|
||||||
|
.. _custom_pages_headers:
|
||||||
|
|
||||||
Custom headers and status codes
|
Custom headers and status codes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
@ -316,6 +335,8 @@ You can verify this is working using ``curl`` like this::
|
||||||
x-teapot: I am
|
x-teapot: I am
|
||||||
content-type: text/html; charset=utf-8
|
content-type: text/html; charset=utf-8
|
||||||
|
|
||||||
|
.. _custom_pages_redirects:
|
||||||
|
|
||||||
Custom redirects
|
Custom redirects
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ def custom_pages_client(tmp_path_factory):
|
||||||
(pages_dir / "redirect2.html").write_text(
|
(pages_dir / "redirect2.html").write_text(
|
||||||
'{{ custom_redirect("/example", 301) }}', "utf-8"
|
'{{ custom_redirect("/example", 301) }}', "utf-8"
|
||||||
)
|
)
|
||||||
|
(pages_dir / "route_{name}.html").write_text(
|
||||||
|
"<p>Hello from {{ name }}</p>", "utf-8"
|
||||||
|
)
|
||||||
nested_dir = pages_dir / "nested"
|
nested_dir = pages_dir / "nested"
|
||||||
nested_dir.mkdir()
|
nested_dir.mkdir()
|
||||||
(nested_dir / "nest.html").write_text("Nest!", "utf-8")
|
(nested_dir / "nest.html").write_text("Nest!", "utf-8")
|
||||||
|
|
@ -83,3 +86,9 @@ def test_redirect2(custom_pages_client):
|
||||||
response = custom_pages_client.get("/redirect2", allow_redirects=False)
|
response = custom_pages_client.get("/redirect2", allow_redirects=False)
|
||||||
assert 301 == response.status
|
assert 301 == response.status
|
||||||
assert "/example" == response.headers["Location"]
|
assert "/example" == response.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_route_pattern(custom_pages_client):
|
||||||
|
response = custom_pages_client.get("/route_Sally")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == "<p>Hello from Sally</p>"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue