From 34eac5e78f0e6342c76aff6ff4b49c23675908ed Mon Sep 17 00:00:00 2001 From: Pablo Tamarit Date: Fri, 13 Dec 2024 16:29:51 +0100 Subject: [PATCH 1/2] views: FAIR signposting remove linkset link to itself --- .../resources/serializers/signposting/schema.py | 2 ++ .../serializers/test_signposting_serializer.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/invenio_rdm_records/resources/serializers/signposting/schema.py b/invenio_rdm_records/resources/serializers/signposting/schema.py index 5c4389cbe..47909c582 100644 --- a/invenio_rdm_records/resources/serializers/signposting/schema.py +++ b/invenio_rdm_records/resources/serializers/signposting/schema.py @@ -75,6 +75,8 @@ def serialize_describedby(self, obj, **kwargs): result = [ {"href": obj["links"]["self"], "type": mimetype} for mimetype in sorted(record_serializers) + # Remove the linkset serializer, so that the linkset does not link to itself. + if mimetype != "application/linkset+json" ] return result or missing diff --git a/tests/resources/serializers/test_signposting_serializer.py b/tests/resources/serializers/test_signposting_serializer.py index 67551d342..9e6467bca 100644 --- a/tests/resources/serializers/test_signposting_serializer.py +++ b/tests/resources/serializers/test_signposting_serializer.py @@ -33,10 +33,10 @@ def test_signposting_serializer_full(running_app, full_record_to_dict): "href": "https://127.0.0.1:5000/api/records/12345-abcde", "type": "application/ld+json", }, - { - "href": "https://127.0.0.1:5000/api/records/12345-abcde", - "type": "application/linkset+json", - }, + # { + # "href": "https://127.0.0.1:5000/api/records/12345-abcde", + # "type": "application/linkset+json", + # }, { "href": "https://127.0.0.1:5000/api/records/12345-abcde", "type": "application/marcxml+xml", @@ -146,10 +146,10 @@ def test_signposting_serializer_minimal(running_app, minimal_record_to_dict): "href": "https://127.0.0.1:5000/api/records/67890-fghij", "type": "application/ld+json", }, - { - "href": "https://127.0.0.1:5000/api/records/67890-fghij", - "type": "application/linkset+json", - }, + # { + # "href": "https://127.0.0.1:5000/api/records/67890-fghij", + # "type": "application/linkset+json", + # }, { "href": "https://127.0.0.1:5000/api/records/67890-fghij", "type": "application/marcxml+xml", From e5046bee68f262255eaed22f8596c9c34a3ea395 Mon Sep 17 00:00:00 2001 From: Pablo Tamarit Date: Mon, 16 Dec 2024 11:31:15 +0100 Subject: [PATCH 2/2] views: FAIR signposting level 1 support --- .../resources/serializers/__init__.py | 8 +- .../serializers/signposting/__init__.py | 31 ++++++- .../serializers/signposting/schema.py | 39 +++++++-- .../test_signposting_serializer.py | 81 +++++++++++++++++-- 4 files changed, 141 insertions(+), 18 deletions(-) diff --git a/invenio_rdm_records/resources/serializers/__init__.py b/invenio_rdm_records/resources/serializers/__init__.py index 3b67a347a..d970d7b71 100644 --- a/invenio_rdm_records/resources/serializers/__init__.py +++ b/invenio_rdm_records/resources/serializers/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020, 2021 CERN. +# Copyright (C) 2020-2024 CERN. # Copyright (C) 2020 Northwestern University. # Copyright (C) 2021 Graz University of Technology. # @@ -34,7 +34,10 @@ ) from .marcxml import MARCXMLSerializer from .schemaorg import SchemaorgJSONLDSerializer -from .signposting import FAIRSignpostingProfileLvl2Serializer +from .signposting import ( + FAIRSignpostingProfileLvl1Serializer, + FAIRSignpostingProfileLvl2Serializer, +) from .ui import UIJSONSerializer __all__ = ( @@ -47,6 +50,7 @@ "DataPackageSerializer", "DublinCoreJSONSerializer", "DublinCoreXMLSerializer", + "FAIRSignpostingProfileLvl1Serializer", "FAIRSignpostingProfileLvl2Serializer", "GeoJSONSerializer", "IIIFCanvasV2JSONSerializer", diff --git a/invenio_rdm_records/resources/serializers/signposting/__init__.py b/invenio_rdm_records/resources/serializers/signposting/__init__.py index 910686b85..4da64a350 100644 --- a/invenio_rdm_records/resources/serializers/signposting/__init__.py +++ b/invenio_rdm_records/resources/serializers/signposting/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 Northwestern University. +# Copyright (C) 2024 CERN. # # 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. @@ -8,9 +9,35 @@ """Signposting serializers.""" from flask_resources import BaseListSchema, MarshmallowSerializer -from flask_resources.serializers import JSONSerializer +from flask_resources.serializers import JSONSerializer, SimpleSerializer -from .schema import FAIRSignpostingProfileLvl2Schema +from .schema import FAIRSignpostingProfileLvl2Schema, LandingPageLvl1Schema + + +class FAIRSignpostingProfileLvl1Serializer(MarshmallowSerializer): + """FAIR Signposting Profile level 1 serializer.""" + + def __init__(self): + """Initialise Serializer.""" + super().__init__( + format_serializer_cls=SimpleSerializer, + object_schema_cls=LandingPageLvl1Schema, + list_schema_cls=BaseListSchema, + encoder=self.fair_signposting_tostring, + ) + + @classmethod + def fair_signposting_tostring(cls, record): + """Stringify a FAIR Signposting record.""" + links = [] + for rel, values in record.items(): + # if rel not in excluded_keys: + for value in values: + link = f'<{value["href"]}> ; rel="{rel}"' + if "type" in value: + link += f' ; type="{value["type"]}"' + links.append(link) + return " , ".join(links) class FAIRSignpostingProfileLvl2Serializer(MarshmallowSerializer): diff --git a/invenio_rdm_records/resources/serializers/signposting/schema.py b/invenio_rdm_records/resources/serializers/signposting/schema.py index 47909c582..65bc345c2 100644 --- a/invenio_rdm_records/resources/serializers/signposting/schema.py +++ b/invenio_rdm_records/resources/serializers/signposting/schema.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 Northwestern University. +# Copyright (C) 2024 CERN. # # 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. @@ -20,7 +21,6 @@ class LandingPageSchema(Schema): Serialization input (`obj`) is a whole record dict projection. """ - anchor = fields.Method(serialize="serialize_anchor") author = fields.Method(serialize="serialize_author") cite_as = fields.Method(data_key="cite-as", serialize="serialize_cite_as") describedby = fields.Method(serialize="serialize_describedby") @@ -28,10 +28,6 @@ class LandingPageSchema(Schema): license = fields.Method(serialize="serialize_license") type = fields.Method(serialize="serialize_type") - def serialize_anchor(self, obj, **kwargs): - """Seralize to landing page URL.""" - return obj["links"]["self_html"] - def serialize_author(self, obj, **kwargs): """Serialize author(s). @@ -144,6 +140,37 @@ def serialize_type(self, obj, **kwargs): return result +class LandingPageLvl1Schema(LandingPageSchema): + """Schema for serialization of link context object for the level 1 landing page. + + Serialization input (`obj`) is a whole record dict projection. + """ + + linkset = fields.Method(serialize="serialize_linkset") + + def serialize_linkset(self, obj, **kwargs): + """Serialize the linkset URL.""" + return [ + { + "href": obj["links"]["self"], + "type": "application/linkset+json", + } + ] + + +class LandingPageLvl2Schema(LandingPageSchema): + """Schema for serialization of link context object for the level 2 landing page. + + Serialization input (`obj`) is a whole record dict projection. + """ + + anchor = fields.Method(serialize="serialize_anchor") + + def serialize_anchor(self, obj, **kwargs): + """Serialize to landing page URL.""" + return obj["links"]["self_html"] + + class ContentResourceSchema(Schema): """Schema for serialization of link context object for the content resource. @@ -205,7 +232,7 @@ class FAIRSignpostingProfileLvl2Schema(Schema): def serialize_linkset(self, obj, **kwargs): """Serialize linkset.""" - result = [LandingPageSchema().dump(obj)] + result = [LandingPageLvl2Schema().dump(obj)] content_resource_schema = ContentResourceSchema(context={"record_dict": obj}) result += [ diff --git a/tests/resources/serializers/test_signposting_serializer.py b/tests/resources/serializers/test_signposting_serializer.py index 9e6467bca..01557a7f6 100644 --- a/tests/resources/serializers/test_signposting_serializer.py +++ b/tests/resources/serializers/test_signposting_serializer.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 Northwestern University. +# Copyright (C) 2024 CERN. # # 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. @@ -8,6 +9,7 @@ """Resources serializers tests.""" from invenio_rdm_records.resources.serializers import ( + FAIRSignpostingProfileLvl1Serializer, FAIRSignpostingProfileLvl2Serializer, ) @@ -33,10 +35,6 @@ def test_signposting_serializer_full(running_app, full_record_to_dict): "href": "https://127.0.0.1:5000/api/records/12345-abcde", "type": "application/ld+json", }, - # { - # "href": "https://127.0.0.1:5000/api/records/12345-abcde", - # "type": "application/linkset+json", - # }, { "href": "https://127.0.0.1:5000/api/records/12345-abcde", "type": "application/marcxml+xml", @@ -125,6 +123,43 @@ def test_signposting_serializer_full(running_app, full_record_to_dict): assert expected == serialized +def test_signposting_lvl1_serializer_full(running_app, full_record_to_dict): + ui_url = "https://127.0.0.1:5000/records/12345-abcde" + api_url = "https://127.0.0.1:5000/api/records/12345-abcde" + filename = "test.txt" + + expected = [ + f' ; rel="author"', + f' ; rel="cite-as"', + 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"', + ' ; rel="license"', + ' ; rel="license"', + ' ; rel="type"', + ' ; rel="type"', + f'<{api_url}> ; rel="linkset" ; type="application/linkset+json"', + ] + + serialized = FAIRSignpostingProfileLvl1Serializer().serialize_object( + full_record_to_dict + ) + + assert expected == serialized.split(" , ") + + def test_signposting_serializer_minimal(running_app, minimal_record_to_dict): expected = { "linkset": [ @@ -146,10 +181,6 @@ def test_signposting_serializer_minimal(running_app, minimal_record_to_dict): "href": "https://127.0.0.1:5000/api/records/67890-fghij", "type": "application/ld+json", }, - # { - # "href": "https://127.0.0.1:5000/api/records/67890-fghij", - # "type": "application/linkset+json", - # }, { "href": "https://127.0.0.1:5000/api/records/67890-fghij", "type": "application/marcxml+xml", @@ -218,3 +249,37 @@ def test_signposting_serializer_minimal(running_app, minimal_record_to_dict): serialized = FAIRSignpostingProfileLvl2Serializer().dump_obj(minimal_record_to_dict) assert expected == serialized + + +def test_signposting_lvl1_serializer_minimal(running_app, minimal_record_to_dict): + api_url = "https://127.0.0.1:5000/api/records/67890-fghij" + + expected = [ + # No author since no associated PID + # No cite-as since 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"', + # No files + # No license + ' ; rel="type"', + ' ; rel="type"', + f'<{api_url}> ; rel="linkset" ; type="application/linkset+json"', + ] + + serialized = FAIRSignpostingProfileLvl1Serializer().serialize_object( + minimal_record_to_dict + ) + + assert expected == serialized.split(" , ")