Skip to content

Commit

Permalink
views: add signposting link to landing+download response headers
Browse files Browse the repository at this point in the history
  • Loading branch information
fenekku committed Mar 19, 2024
1 parent 4b8dcfc commit 4cd385f
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 7 deletions.
30 changes: 23 additions & 7 deletions invenio_app_rdm/records_ui/views/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from functools import wraps

from flask import g, redirect, request, url_for
from flask import g, make_response, redirect, request, url_for
from invenio_communities.communities.resources.serializer import (
UICommunityJSONSerializer,
)
Expand All @@ -21,6 +21,8 @@
from invenio_records_resources.services.errors import PermissionDeniedError
from sqlalchemy.orm.exc import NoResultFound

from invenio_app_rdm.urls import record_url_for


def service():
"""Get the record service."""
Expand Down Expand Up @@ -103,11 +105,7 @@ def pass_is_preview(f):

@wraps(f)
def view(**kwargs):
preview = request.args.get("preview")
is_preview = False
if preview == "1":
is_preview = True
kwargs["is_preview"] = is_preview
kwargs["is_preview"] = request.args.get("preview") == "1"
return f(**kwargs)

return view
Expand Down Expand Up @@ -334,7 +332,6 @@ def view(**kwargs):
pid_value = kwargs.get("pid_value")
files = draft_files_service().list_files(id_=pid_value, identity=g.identity)
kwargs["draft_files"] = files

except PermissionDeniedError:
# this is handled here because we don't want a 404 on the landing
# page when a user is allowed to read the metadata but not the
Expand Down Expand Up @@ -365,3 +362,22 @@ def view(**kwargs):
return f(**kwargs)

return view


def add_signposting(f):
"""Add signposting link to 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
)
return response

return view
3 changes: 3 additions & 0 deletions invenio_app_rdm/records_ui/views/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

from ..utils import get_external_resources
from .decorators import (
add_signposting,
pass_file_item,
pass_file_metadata,
pass_include_deleted,
Expand Down Expand Up @@ -139,6 +140,7 @@ def open(self):
@pass_record_or_draft(expand=True)
@pass_record_files
@pass_record_media_files
@add_signposting
def record_detail(
pid_value, record, files, media_files, is_preview=False, include_deleted=False
):
Expand Down Expand Up @@ -300,6 +302,7 @@ def record_file_preview(

@pass_is_preview
@pass_file_item(is_media=False)
@add_signposting
def record_file_download(pid_value, file_item=None, is_preview=False, **kwargs):
"""Download a file from a record."""
download = bool(request.args.get("download"))
Expand Down
62 changes: 62 additions & 0 deletions invenio_app_rdm/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 Northwestern University.
#
# This file is lifted from Invenio-RDM-Records .
#
# Invenio-RDM-Records is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""Convenient URL generation.
InvenioRDM poses challenges to url generation that Flask's url_for cannot handle out
of the gate.
- InvenioRDM is actually 2 applications mounted on different url_prefixes:
`url_for` in the API application isn't aware of the UI application endpoints
- The endpoint names are relatively hidden / spread out and APP_RDM_ROUTES is usually
the interface to name them.
- Need to be able to generate urls outside of request context without thinking about it,
This module contains minimal methods to generate URLs correctly without much
engineering. Over time, it can be made more abstract, complex and powerful and even
extracted into its own package to solve url generation across InvenioRDM once and for
all.
Design decisions:
- Generated urls are absolute for now
"""

from flask import current_app


def record_url_for(_app="ui", pid_value=""):
"""Return url for record route."""
assert _app in ["ui", "api"]

site_app = _app.upper()
url_prefix = current_app.config.get(f"SITE_{site_app}_URL", "")

# We use [] so that this fails and brings to attention the configuration
# problem if APP_RDM_ROUTES.record_detail is missing
url_path = current_app.config["APP_RDM_ROUTES"]["record_detail"].replace(
"<pid_value>", pid_value
)

return "/".join(p.strip("/") for p in [url_prefix, url_path])


def download_url_for(pid_value="", filename=""):
"""Return url for download route."""
url_prefix = current_app.config.get(f"SITE_UI_URL", "")

# We use [] so that this fails and brings to attention the configuration
# problem if APP_RDM_ROUTES.record_file_download is missing
url_path = (
current_app.config["APP_RDM_ROUTES"]["record_file_download"]
.replace("<pid_value>", pid_value)
.replace("<path:filename>", filename)
)

return "/".join(p.strip("/") for p in [url_prefix, url_path])
34 changes: 34 additions & 0 deletions tests/ui/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

"""Pytest fixtures and plugins for the UI application."""

from io import BytesIO

import pytest
from flask_webpackext.manifest import (
JinjaManifest,
Expand Down Expand Up @@ -68,3 +70,35 @@ def record(running_app, minimal_record):
s = current_rdm_records.records_service
draft = s.create(system_identity, minimal_record)
return s.publish(system_identity, draft.id)


@pytest.fixture()
def record_with_file(running_app, minimal_record):
"""Create and publish a record with file."""
minimal_record["files"] = {"enabled": True}

record_service = current_rdm_records.records_service
file_service = record_service.draft_files

draft = record_service.create(system_identity, minimal_record)
file_to_initialise = [
{
"key": "article.txt",
"checksum": "md5:c785060c866796cc2a1708c997154c8e",
"size": 17, # 2kB
"metadata": {
"description": "Published article PDF.",
},
}
]
file_service.init_files(system_identity, draft.id, file_to_initialise)
content = BytesIO(b"test file content")
file_service.set_file_content(
system_identity,
draft.id,
file_to_initialise[0]["key"],
content,
content.getbuffer().nbytes,
)
file_service.commit_file(system_identity, draft.id, "article.txt")
return record_service.publish(system_identity, draft.id)
33 changes: 33 additions & 0 deletions tests/ui/test_signposting_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 Northwestern University.
#
# Invenio-App-RDM is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""Test Signposting.
See https://signposting.org/FAIR/#level2 for more information on Signposting
"""


def test_link_in_landing_page_response_headers(running_app, client, record):
res = client.head(f"/records/{record.id}")

assert (
res.headers["Link"]
== f'<https://127.0.0.1:5000/api/records/{record.id}> ; rel="linkset" ; type="application/linkset+json"' # noqa
)


def test_link_in_content_resource_response_headers(
running_app, client, record_with_file
):
filename = "article.txt"

res = client.head(f"/records/{record_with_file.id}/files/{filename}")

assert (
res.headers["Link"]
== f'<https://127.0.0.1:5000/api/records/{record_with_file.id}> ; rel="linkset" ; type="application/linkset+json"' # noqa
)

0 comments on commit 4cd385f

Please sign in to comment.