From 5aa4f2b89324e65e8c5e9c4fa2a23215e818b39e Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 16 Feb 2026 20:46:10 +0200 Subject: [PATCH 1/3] add link header to guid metadata endpoints for registry and project --- website/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/views.py b/website/views.py index 24d8007cd9d..50f576f75f7 100644 --- a/website/views.py +++ b/website/views.py @@ -423,4 +423,8 @@ def guid_metadata_download(guid, resource, metadata_format): def metadata_download(guid): format_arg = request.args.get('format', 'datacite-json') resource = Guid.load(guid) - return guid_metadata_download(guid, resource=resource, metadata_format=format_arg) + is_resource_project_or_registration = isinstance(resource.referent, (Node, Registration)) + response = guid_metadata_download(guid, resource=resource, metadata_format=format_arg) + if is_resource_project_or_registration: + response.headers['link'] = f'{settings.DOMAIN}{guid}/>; rel="describes"; type="text/html"' + return response From 30ee2e6082734ddff3b958b95ddb32ee1e1b87da Mon Sep 17 00:00:00 2001 From: mkovalua Date: Mon, 16 Feb 2026 20:51:40 +0200 Subject: [PATCH 2/3] update code --- website/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/views.py b/website/views.py index 50f576f75f7..701f813d59b 100644 --- a/website/views.py +++ b/website/views.py @@ -426,5 +426,5 @@ def metadata_download(guid): is_resource_project_or_registration = isinstance(resource.referent, (Node, Registration)) response = guid_metadata_download(guid, resource=resource, metadata_format=format_arg) if is_resource_project_or_registration: - response.headers['link'] = f'{settings.DOMAIN}{guid}/>; rel="describes"; type="text/html"' + response.headers['link'] = f'<{settings.DOMAIN}{guid}/>; rel="describes"; type="text/html"' return response From 913f55b208f08907b6fd60687a7796a8cd54c151 Mon Sep 17 00:00:00 2001 From: mkovalua Date: Wed, 18 Feb 2026 19:34:32 +0200 Subject: [PATCH 3/3] add link header to cedar metadata records endpoints --- api/cedar_metadata_records/views.py | 11 ++++++++--- .../views/test_record_metadata_download_get.py | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/api/cedar_metadata_records/views.py b/api/cedar_metadata_records/views.py index 86eec237804..33ae361334f 100644 --- a/api/cedar_metadata_records/views.py +++ b/api/cedar_metadata_records/views.py @@ -19,8 +19,8 @@ CedarMetadataRecordsDetailSerializer, ) from framework.auth.oauth_scopes import CoreScopes - -from osf.models import CedarMetadataRecord +from osf.models import CedarMetadataRecord, Node, Registration +from website import settings logger = logging.getLogger(__name__) @@ -99,5 +99,10 @@ def get_serializer_class(self): def get(self, request, *args, **kwargs): record = self.get_object() + is_referent_project_or_registration = isinstance(record.guid.referent, (Node, Registration)) file_name = f'{record._id}-{record.get_template_name()}-v{record.get_template_version()}.json' - return Response(record.metadata, headers={'Content-Disposition': f'attachment; filename={file_name}'}) + headers = {'Content-Disposition': f'attachment; filename={file_name}'} + if is_referent_project_or_registration: + guid_id = record.guid._id + headers['link'] = f'<{settings.DOMAIN}{guid_id}/>; rel="describes"; type="text/html"' + return Response(record.metadata, headers=headers) diff --git a/api_tests/cedar_metadata_records/views/test_record_metadata_download_get.py b/api_tests/cedar_metadata_records/views/test_record_metadata_download_get.py index eb10a25aa8c..81cb97852f5 100644 --- a/api_tests/cedar_metadata_records/views/test_record_metadata_download_get.py +++ b/api_tests/cedar_metadata_records/views/test_record_metadata_download_get.py @@ -3,6 +3,8 @@ from .test_record import TestCedarMetadataRecord from osf.utils.permissions import READ, WRITE from osf_tests.factories import AuthUserFactory +from website import settings + @pytest.mark.django_db class TestCedarMetadataRecordMetadataDownloadPrivateProjectPublishedMetadata(TestCedarMetadataRecord): @@ -13,6 +15,7 @@ def test_record_metadata_download_for_node_with_admin_auth(self, app, node, user resp = app.get(f'/_/cedar_metadata_records/{cedar_record_for_node._id}/metadata_download/', auth=admin.auth) assert resp.status_code == 200 assert resp.headers['Content-Disposition'] == f'attachment; filename={self.get_record_metadata_download_file_name(cedar_record_for_node)}' + assert resp.headers.get('Link') == f'<{settings.DOMAIN}{node._id}/>; rel="describes"; type="text/html"' assert resp.json == cedar_record_metadata_json def test_record_metadata_download_for_node_with_write_auth(self, app, node, cedar_record_for_node, cedar_record_metadata_json): @@ -179,6 +182,7 @@ def test_record_metadata_download_for_registration_with_admin_auth(self, app, us resp = app.get(f'/_/cedar_metadata_records/{cedar_record_for_registration._id}/metadata_download/', auth=admin.auth) assert resp.status_code == 200 assert resp.headers['Content-Disposition'] == f'attachment; filename={self.get_record_metadata_download_file_name(cedar_record_for_registration)}' + assert resp.headers.get('Link') == f'<{settings.DOMAIN}{cedar_record_for_registration.guid._id}/>; rel="describes"; type="text/html"' assert resp.json == cedar_record_metadata_json def test_record_metadata_download_for_registration_with_write_auth(self, app, registration, cedar_record_for_registration, cedar_record_metadata_json): @@ -307,6 +311,7 @@ def test_record_metadata_download_for_node_with_admin_auth(self, app, user, ceda resp = app.get(f'/_/cedar_metadata_records/{cedar_draft_record_for_file_alt._id}/metadata_download/', auth=admin.auth) assert resp.status_code == 200 assert resp.headers['Content-Disposition'] == f'attachment; filename={self.get_record_metadata_download_file_name(cedar_draft_record_for_file_alt)}' + assert not resp.headers.get('Link') assert resp.json == cedar_record_metadata_json def test_record_metadata_download_for_node_with_write_auth(self, app, node_alt, cedar_draft_record_for_file_alt, cedar_record_metadata_json):