mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Link rel=alternate header for tables and rows
Also added Access-Control-Expose-Headers: Link to --cors mode. Closes #1533 Refs https://github.com/simonw/datasette-notebook/issues/2 LL# metadata.json.1
This commit is contained in:
parent
2aa686c655
commit
3ef47a0896
9 changed files with 86 additions and 6 deletions
|
|
@ -10,7 +10,7 @@
|
||||||
{% for url in extra_js_urls %}
|
{% for url in extra_js_urls %}
|
||||||
<script {% if url.module %}type="module" {% endif %}src="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script>
|
<script {% if url.module %}type="module" {% endif %}src="{{ url.url }}"{% if url.sri %} integrity="{{ url.sri }}" crossorigin="anonymous"{% endif %}></script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% block extra_head %}{% endblock %}
|
{%- block extra_head %}{% endblock -%}
|
||||||
</head>
|
</head>
|
||||||
<body class="{% block body_class %}{% endblock %}">
|
<body class="{% block body_class %}{% endblock %}">
|
||||||
<div class="not-footer">
|
<div class="not-footer">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
{% block title %}{{ database }}: {{ table }}{% endblock %}
|
{% block title %}{{ database }}: {{ table }}{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{{ super() }}
|
{{- super() -}}
|
||||||
|
<link rel="alternate" type="application/json+datasette" href="{{ alternate_url_json }}">
|
||||||
<style>
|
<style>
|
||||||
@media only screen and (max-width: 576px) {
|
@media only screen and (max-width: 576px) {
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
{% block title %}{{ database }}: {{ table }}: {% if filtered_table_rows_count or filtered_table_rows_count == 0 %}{{ "{:,}".format(filtered_table_rows_count) }} row{% if filtered_table_rows_count == 1 %}{% else %}s{% endif %}{% endif %}{% if human_description_en %} {{ human_description_en }}{% endif %}{% endblock %}
|
{% block title %}{{ database }}: {{ table }}: {% if filtered_table_rows_count or filtered_table_rows_count == 0 %}{{ "{:,}".format(filtered_table_rows_count) }} row{% if filtered_table_rows_count == 1 %}{% else %}s{% endif %}{% endif %}{% if human_description_en %} {{ human_description_en }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{{ super() }}
|
{{- super() -}}
|
||||||
|
<link rel="alternate" type="application/json+datasette" href="{{ alternate_url_json }}">
|
||||||
<script src="{{ urls.static('table.js') }}" defer></script>
|
<script src="{{ urls.static('table.js') }}" defer></script>
|
||||||
<style>
|
<style>
|
||||||
@media only screen and (max-width: 576px) {
|
@media only screen and (max-width: 576px) {
|
||||||
|
|
|
||||||
|
|
@ -1094,3 +1094,4 @@ async def derive_named_parameters(db, sql):
|
||||||
def add_cors_headers(headers):
|
def add_cors_headers(headers):
|
||||||
headers["Access-Control-Allow-Origin"] = "*"
|
headers["Access-Control-Allow-Origin"] = "*"
|
||||||
headers["Access-Control-Allow-Headers"] = "Authorization"
|
headers["Access-Control-Allow-Headers"] = "Authorization"
|
||||||
|
headers["Access-Control-Expose-Headers"] = "Link"
|
||||||
|
|
|
||||||
|
|
@ -137,10 +137,18 @@ class BaseView:
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
# Hacky cheat to add extra headers
|
||||||
|
headers = {}
|
||||||
|
if "_extra_headers" in context:
|
||||||
|
headers.update(context["_extra_headers"])
|
||||||
return Response.html(
|
return Response.html(
|
||||||
await self.ds.render_template(
|
await self.ds.render_template(
|
||||||
template, template_context, request=request, view_name=self.name
|
template,
|
||||||
)
|
template_context,
|
||||||
|
request=request,
|
||||||
|
view_name=self.name,
|
||||||
|
),
|
||||||
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from datasette.utils import (
|
||||||
is_url,
|
is_url,
|
||||||
path_from_row_pks,
|
path_from_row_pks,
|
||||||
path_with_added_args,
|
path_with_added_args,
|
||||||
|
path_with_format,
|
||||||
path_with_removed_args,
|
path_with_removed_args,
|
||||||
path_with_replaced_args,
|
path_with_replaced_args,
|
||||||
to_css_class,
|
to_css_class,
|
||||||
|
|
@ -850,7 +851,12 @@ class TableView(RowTableShared):
|
||||||
for table_column in table_columns
|
for table_column in table_columns
|
||||||
if table_column not in columns
|
if table_column not in columns
|
||||||
]
|
]
|
||||||
|
alternate_url_json = self.ds.absolute_url(
|
||||||
|
request,
|
||||||
|
self.ds.urls.path(path_with_format(request=request, format="json")),
|
||||||
|
)
|
||||||
d = {
|
d = {
|
||||||
|
"alternate_url_json": alternate_url_json,
|
||||||
"table_actions": table_actions,
|
"table_actions": table_actions,
|
||||||
"use_rowid": use_rowid,
|
"use_rowid": use_rowid,
|
||||||
"filters": filters,
|
"filters": filters,
|
||||||
|
|
@ -881,6 +887,11 @@ class TableView(RowTableShared):
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
"view_definition": await db.get_view_definition(table),
|
"view_definition": await db.get_view_definition(table),
|
||||||
"table_definition": await db.get_table_definition(table),
|
"table_definition": await db.get_table_definition(table),
|
||||||
|
"_extra_headers": {
|
||||||
|
"Link": '{}; rel="alternate"; type="application/json+datasette"'.format(
|
||||||
|
alternate_url_json
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
d.update(extra_context_from_filters)
|
d.update(extra_context_from_filters)
|
||||||
return d
|
return d
|
||||||
|
|
@ -964,8 +975,12 @@ class RowView(RowTableShared):
|
||||||
)
|
)
|
||||||
for column in display_columns:
|
for column in display_columns:
|
||||||
column["sortable"] = False
|
column["sortable"] = False
|
||||||
|
alternate_url_json = self.ds.absolute_url(
|
||||||
|
request,
|
||||||
|
self.ds.urls.path(path_with_format(request=request, format="json")),
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
|
"alternate_url_json": alternate_url_json,
|
||||||
"foreign_key_tables": await self.foreign_key_tables(
|
"foreign_key_tables": await self.foreign_key_tables(
|
||||||
database, table, pk_values
|
database, table, pk_values
|
||||||
),
|
),
|
||||||
|
|
@ -980,6 +995,11 @@ class RowView(RowTableShared):
|
||||||
.get(database, {})
|
.get(database, {})
|
||||||
.get("tables", {})
|
.get("tables", {})
|
||||||
.get(table, {}),
|
.get(table, {}),
|
||||||
|
"_extra_headers": {
|
||||||
|
"Link": '{}; rel="alternate"; type="application/json+datasette"'.format(
|
||||||
|
alternate_url_json
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ served with the following additional HTTP headers::
|
||||||
|
|
||||||
Access-Control-Allow-Origin: *
|
Access-Control-Allow-Origin: *
|
||||||
Access-Control-Allow-Headers: Authorization
|
Access-Control-Allow-Headers: Authorization
|
||||||
|
Access-Control-Expose-Headers: Link
|
||||||
|
|
||||||
This means JavaScript running on any domain will be able to make cross-origin
|
This means JavaScript running on any domain will be able to make cross-origin
|
||||||
requests to fetch the data.
|
requests to fetch the data.
|
||||||
|
|
@ -435,3 +436,22 @@ looks like::
|
||||||
|
|
||||||
The column in the foreign key table that is used for the label can be specified
|
The column in the foreign key table that is used for the label can be specified
|
||||||
in ``metadata.json`` - see :ref:`label_columns`.
|
in ``metadata.json`` - see :ref:`label_columns`.
|
||||||
|
|
||||||
|
.. _json_api_discover_alternate:
|
||||||
|
|
||||||
|
Discovering the JSON for a page
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
The :ref:`table <TableView>` and :ref:`row <RowView>` HTML pages both provide a mechanism for discovering their JSON equivalents using the HTML ``link`` mechanism.
|
||||||
|
|
||||||
|
You can find this near the top of those pages, looking like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
<link rel="alternate"
|
||||||
|
type="application/json+datasette"
|
||||||
|
href="https://latest.datasette.io/fixtures/sortable.json">
|
||||||
|
|
||||||
|
The JSON URL is also made available in a ``Link`` HTTP header for the page::
|
||||||
|
|
||||||
|
Link: https://latest.datasette.io/fixtures/sortable.json; rel="alternate"; type="application/json+datasette"
|
||||||
|
|
|
||||||
|
|
@ -977,6 +977,7 @@ def test_cors(app_client_with_cors, path, status_code):
|
||||||
assert response.status == status_code
|
assert response.status == status_code
|
||||||
assert response.headers["Access-Control-Allow-Origin"] == "*"
|
assert response.headers["Access-Control-Allow-Origin"] == "*"
|
||||||
assert response.headers["Access-Control-Allow-Headers"] == "Authorization"
|
assert response.headers["Access-Control-Allow-Headers"] == "Authorization"
|
||||||
|
assert response.headers["Access-Control-Expose-Headers"] == "Link"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
||||||
|
|
@ -1069,3 +1069,31 @@ def test_table_page_title(app_client, path, expected):
|
||||||
response = app_client.get(path)
|
response = app_client.get(path)
|
||||||
title = Soup(response.text, "html.parser").find("title").text
|
title = Soup(response.text, "html.parser").find("title").text
|
||||||
assert title == expected
|
assert title == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path,expected",
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"/fixtures/table%2Fwith%2Fslashes.csv",
|
||||||
|
"http://localhost/fixtures/table%2Fwith%2Fslashes.csv?_format=json",
|
||||||
|
),
|
||||||
|
("/fixtures/facetable", "http://localhost/fixtures/facetable.json"),
|
||||||
|
(
|
||||||
|
"/fixtures/no_primary_key/1",
|
||||||
|
"http://localhost/fixtures/no_primary_key/1.json",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_alternate_url_json(app_client, path, expected):
|
||||||
|
response = app_client.get(path)
|
||||||
|
link = response.headers["link"]
|
||||||
|
assert link == '{}; rel="alternate"; type="application/json+datasette"'.format(
|
||||||
|
expected
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
'<link rel="alternate" type="application/json+datasette" href="{}">'.format(
|
||||||
|
expected
|
||||||
|
)
|
||||||
|
in response.text
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue