-
Notifications
You must be signed in to change notification settings - Fork 150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
views: FAIR signposting level 1 support (HTTP Link headers) #2938
base: master
Are you sure you want to change the base?
Changes from all commits
0042190
15672de
17401b6
2e533ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,14 +11,15 @@ | |
|
||
from functools import wraps | ||
|
||
from flask import g, make_response, redirect, request, session, url_for | ||
from flask import current_app, g, make_response, redirect, request, session, url_for | ||
from flask_login import login_required | ||
from invenio_communities.communities.resources.serializer import ( | ||
UICommunityJSONSerializer, | ||
) | ||
from invenio_communities.proxies import current_communities | ||
from invenio_pidstore.errors import PIDDoesNotExistError | ||
from invenio_rdm_records.proxies import current_rdm_records | ||
from invenio_rdm_records.resources.serializers.signposting import FAIRSignpostingProfileLvl1Serializer | ||
from invenio_records_resources.services.errors import PermissionDeniedError | ||
from sqlalchemy.orm.exc import NoResultFound | ||
|
||
|
@@ -365,20 +366,86 @@ def view(**kwargs): | |
return view | ||
|
||
|
||
def add_signposting(f): | ||
"""Add signposting link to view's response headers.""" | ||
def _get_header(rel, value, link_type=None): | ||
header = f'<{value}> ; rel="{rel}"' | ||
if link_type: | ||
header += f' ; type="{link_type}"' | ||
return header | ||
|
||
|
||
def _get_signposting_collection(pid_value): | ||
ui_url = record_url_for(pid_value=pid_value) | ||
return _get_header("collection", ui_url, "text/html") | ||
|
||
|
||
def _get_signposting_describes(pid_value): | ||
ui_url = record_url_for(pid_value=pid_value) | ||
return _get_header("describes", ui_url, "text/html") | ||
|
||
|
||
def _get_signposting_linkset(pid_value): | ||
api_url = record_url_for(_app="api", pid_value=pid_value) | ||
return _get_header("linkset", api_url, "application/linkset+json") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: this is required for level 2 support and was already added in a previous pull request. |
||
|
||
|
||
def add_signposting_landing_page(f): | ||
"""Add signposting links to the landing page view's response headers.""" | ||
|
||
@wraps(f) | ||
def view(*args, **kwargs): | ||
response = make_response(f(*args, **kwargs)) | ||
|
||
# Relies on other decorators having operated before it | ||
record = kwargs["record"] | ||
|
||
signposting_headers = FAIRSignpostingProfileLvl1Serializer().serialize_object(record.to_dict()) | ||
|
||
response.headers["Link"] = signposting_headers | ||
|
||
return response | ||
|
||
return view | ||
|
||
|
||
def add_signposting_content_resources(f): | ||
"""Add signposting links to the content resources view's response headers.""" | ||
|
||
@wraps(f) | ||
def view(*args, **kwargs): | ||
response = make_response(f(*args, **kwargs)) | ||
|
||
# Relies on other decorators having operated before it | ||
pid_value = kwargs["pid_value"] | ||
signposting_link = record_url_for(_app="api", pid_value=pid_value) | ||
|
||
response.headers["Link"] = ( | ||
f'<{signposting_link}> ; rel="linkset" ; type="application/linkset+json"' # fmt: skip | ||
) | ||
signposting_headers = [ | ||
_get_signposting_collection(pid_value), | ||
_get_signposting_linkset(pid_value), | ||
] | ||
|
||
response.headers["Link"] = " , ".join(signposting_headers) | ||
|
||
return response | ||
|
||
return view | ||
|
||
|
||
def add_signposting_metadata_resources(f): | ||
"""Add signposting links to the metadata resources view's response headers.""" | ||
|
||
@wraps(f) | ||
def view(*args, **kwargs): | ||
response = make_response(f(*args, **kwargs)) | ||
|
||
# Relies on other decorators having operated before it | ||
pid_value = kwargs["pid_value"] | ||
|
||
signposting_headers = [ | ||
_get_signposting_describes(pid_value), | ||
_get_signposting_linkset(pid_value), | ||
] | ||
|
||
response.headers["Link"] = " , ".join(signposting_headers) | ||
|
||
Comment on lines
+410
to
+448
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that, unlike the Landing Page which relies on
|
||
return response | ||
|
||
return view | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,25 +9,73 @@ | |
|
||
See https://signposting.org/FAIR/#level2 for more information on Signposting | ||
""" | ||
import pytest | ||
|
||
|
||
def test_link_in_landing_page_response_headers(running_app, client, record): | ||
res = client.head(f"/records/{record.id}") | ||
@pytest.mark.parametrize("http_method", ["head", "get"]) | ||
def test_link_in_landing_page_response_headers( | ||
running_app, client, record_with_file, http_method | ||
): | ||
ui_url = f"https://127.0.0.1:5000/records/{record_with_file.id}" | ||
api_url = f"https://127.0.0.1:5000/api/records/{record_with_file.id}" | ||
filename = "article.txt" | ||
|
||
assert ( | ||
res.headers["Link"] | ||
== f'<https://127.0.0.1:5000/api/records/{record.id}> ; rel="linkset" ; type="application/linkset+json"' # noqa | ||
) | ||
client_http_method = getattr(client, http_method) | ||
res = client_http_method(f"/records/{record_with_file.id}") | ||
|
||
assert res.headers["Link"].split(" , ") == [ | ||
# The test record does not have an author with an identifier. | ||
# The test record does not have a cite-as since it has no DOI. | ||
f'<{api_url}> ; rel="describedby" ; type="application/dcat+xml"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/json"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/ld+json"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/marcxml+xml"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.citationstyles.csl+json"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.datacite.datacite+json"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.datacite.datacite+xml"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.geo+json"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.inveniordm.v1+json"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.inveniordm.v1.full+csv"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/vnd.inveniordm.v1.simple+csv"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/x-bibtex"', | ||
f'<{api_url}> ; rel="describedby" ; type="application/x-dc+xml"', | ||
f'<{api_url}> ; rel="describedby" ; type="text/x-bibliography"', | ||
f'<{ui_url}/files/{filename}> ; rel="item" ; type="text/plain"', | ||
# The test record does not have a license. | ||
'<https://schema.org/Photograph> ; rel="type"', | ||
'<https://schema.org/AboutPage> ; rel="type"', | ||
f'<{api_url}> ; rel="linkset" ; type="application/linkset+json"', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for the landing page is implemented in |
||
] | ||
|
||
|
||
@pytest.mark.parametrize("http_method", ["head", "get"]) | ||
def test_link_in_content_resource_response_headers( | ||
running_app, client, record_with_file | ||
running_app, client, record_with_file, http_method | ||
): | ||
ui_url = f"https://127.0.0.1:5000/records/{record_with_file.id}" | ||
api_url = f"https://127.0.0.1:5000/api/records/{record_with_file.id}" | ||
filename = "article.txt" | ||
|
||
res = client.head(f"/records/{record_with_file.id}/files/{filename}") | ||
client_http_method = getattr(client, http_method) | ||
res = client_http_method(f"/records/{record_with_file.id}/files/{filename}") | ||
|
||
assert res.headers["Link"].split(" , ") == [ | ||
f'<{ui_url}> ; rel="collection" ; type="text/html"', | ||
f'<{api_url}> ; rel="linkset" ; type="application/linkset+json"', | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("http_method", ["head", "get"]) | ||
def test_link_in_metadata_resource_response_headers( | ||
running_app, client, record, http_method | ||
): | ||
ui_url = f"https://127.0.0.1:5000/records/{record.id}" | ||
api_url = f"https://127.0.0.1:5000/api/records/{record.id}" | ||
|
||
client_http_method = getattr(client, http_method) | ||
res = client_http_method(f"/records/{record.id}/export/bibtex") | ||
|
||
assert ( | ||
res.headers["Link"] | ||
== f'<https://127.0.0.1:5000/api/records/{record_with_file.id}> ; rel="linkset" ; type="application/linkset+json"' # noqa | ||
) | ||
assert res.headers["Link"].split(" , ") == [ | ||
f'<{ui_url}> ; rel="describes" ; type="text/html"', | ||
f'<{api_url}> ; rel="linkset" ; type="application/linkset+json"', | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite a lot of methods added to
decorators.py
, should it be moved to a signposting-specific file?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Agree, I thought the was already some signposting-related directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[ ] Move the code to a signposting-related file or directory.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is now much less code in
decorators.py
now that I rely oninvenio_rdm_records/resources/serializers/signposting/schema.py
.