diff --git a/cds_migrator_kit/errors.py b/cds_migrator_kit/errors.py index 5cbe912d..a998ac4a 100644 --- a/cds_migrator_kit/errors.py +++ b/cds_migrator_kit/errors.py @@ -85,3 +85,9 @@ class RecordFlaggedCuration(CDSMigrationException): """Record statistics error.""" description = "[Record needs to be curated]" + + +class MissingConfiguration(CDSMigrationException): + """Missing configuration exception.""" + + description = "[Missing configuration]" diff --git a/cds_migrator_kit/rdm/migration_config.py b/cds_migrator_kit/rdm/migration_config.py index ed243d76..0d9f66ec 100644 --- a/cds_migrator_kit/rdm/migration_config.py +++ b/cds_migrator_kit/rdm/migration_config.py @@ -469,6 +469,11 @@ def resolve_record_pid(pid): """Absolute path to the vocabularies directory. Defaults to {instance_path}/app_data/vocabularies when None.""" +CDS_CERN_SCIENTIFIC_COMMUNITY_ID = os.environ.get( + "CDS_CERN_SCIENTIFIC_COMMUNITY_ID", None +) +"""Community slug or UUID for CERN Research.""" + CDS_ACCESS_GROUP_MAPPINGS = { "SSO": ["cern-accounts-primary"], "ITDepRestrFile": ["it-dep"], diff --git a/cds_migrator_kit/rdm/records/transform/config.py b/cds_migrator_kit/rdm/records/transform/config.py index 4a0f7f3b..6114d59c 100644 --- a/cds_migrator_kit/rdm/records/transform/config.py +++ b/cds_migrator_kit/rdm/records/transform/config.py @@ -152,3 +152,19 @@ FILE_SUBFORMATS_TO_DROP = ["pdfa", "unstamped"] + +# Public research publication resource types that are auto-included in the CERN Research community. +CDS_CERN_SCIENTIFIC_RESOURCE_TYPES = { + "publication-dissertation", # Already included by the migrator for thesis records. + "publication-book", + "publication-section", + "publication-conferencepaper", + "publication-conferenceproceeding", + "publication-journal", + "publication-article", + "publication-preprint", + "publication-report", + "publication-technicalnote", + "publication-note", + "publication", +} diff --git a/cds_migrator_kit/rdm/records/transform/transform.py b/cds_migrator_kit/rdm/records/transform/transform.py index e5439114..f555cef1 100644 --- a/cds_migrator_kit/rdm/records/transform/transform.py +++ b/cds_migrator_kit/rdm/records/transform/transform.py @@ -6,7 +6,7 @@ # the terms of the MIT License; see LICENSE file for more details. """CDS-RDM transform step module.""" -import datetime + import logging from collections import OrderedDict from copy import deepcopy @@ -32,11 +32,12 @@ from cds_migrator_kit.errors import ( ManualImportRequired, + MissingConfiguration, MissingRequiredField, + MultipleModelsMatched, RecordFlaggedCuration, RestrictedFileDetected, UnexpectedValue, - MultipleModelsMatched, ) from cds_migrator_kit.rdm.migration_config import ( RDM_RECORDS_IDENTIFIERS_SCHEMES, @@ -48,6 +49,7 @@ IDENTIFIERS_VALUES_TO_DROP, PIDS_SCHEMES_ALLOWED, PIDS_SCHEMES_TO_DROP, + CDS_CERN_SCIENTIFIC_RESOURCE_TYPES, ) from cds_migrator_kit.transform.dumper import CDSRecordDump from cds_migrator_kit.transform.errors import LossyConversion @@ -793,9 +795,32 @@ def __init__( self.db_state = {"affiliations": CDSMigrationAffiliationMapping} super().__init__(workers, throw) + def _should_add_scientific_community(self, record): + if self.restricted or record.get("access") != "public": + return False + resource_type_id = ( + record.get("json", {}) + .get("metadata", {}) + .get("resource_type", {}) + .get("id") + ) + return resource_type_id in CDS_CERN_SCIENTIFIC_RESOURCE_TYPES + def _communities_ids(self, entry, record): communities = record.get("communities", []) communities = self.communities_ids + [slug for slug in communities] + + scientific_community = current_app.config.get( + "CDS_CERN_SCIENTIFIC_COMMUNITY_ID" + ) + if not scientific_community: + raise MissingConfiguration( + "CDS_CERN_SCIENTIFIC_COMMUNITY_ID is not configured" + ) + if self._should_add_scientific_community(record): + if scientific_community not in communities: + communities.append(scientific_community) + if communities: return {"ids": communities, "default": self.communities_ids[0]} return {} @@ -849,6 +874,7 @@ def _transform(self, entry): ManualImportRequired, MissingRequiredField, MultipleModelsMatched, + MissingConfiguration, ) as e: migration_logger.add_log(e, record=entry) diff --git a/tests/cds-rdm/conftest.py b/tests/cds-rdm/conftest.py index 60b38905..a48ba3ea 100644 --- a/tests/cds-rdm/conftest.py +++ b/tests/cds-rdm/conftest.py @@ -1652,6 +1652,18 @@ def community(running_app, db): return comm +@pytest.fixture() +def cern_scientific_community(running_app, db, app): + """A CERN Research community fixture.""" + comm = Community.create({}) + comm.slug = "cern-research" + comm.metadata = {"title": "CERN Research"} + comm.commit() + db.session.commit() + app.config["CDS_CERN_SCIENTIFIC_COMMUNITY_ID"] = str(comm.id) + return comm + + # @pytest.fixture() # def users(app, db): # """Create example user.""" diff --git a/tests/cds-rdm/test_full_migration.py b/tests/cds-rdm/test_full_migration.py index a444fcc9..b3d12086 100644 --- a/tests/cds-rdm/test_full_migration.py +++ b/tests/cds-rdm/test_full_migration.py @@ -439,6 +439,7 @@ def test_full_migration_stream( superuser_identity, orcid_name_data, community, + cern_scientific_community, # Fixture for the CERN Scientific community, so that it gets auto included for PUBLIC-ations. mocker, groups, ): @@ -494,6 +495,8 @@ def test_full_migration_stream( if record["legacy_recid"] == "2294138": author_with_inspire(loaded_rec) + # Check if the record is also included in the CERN Scientific community since it is a publication report + scientific_community_inclusion(loaded_rec, cern_scientific_community.id) # Check if remote account has the correct metadata # Check if user profile has the correct metadata user_metadata() @@ -563,3 +566,9 @@ def user_metadata(): "person_id": "11115", "department": "IT", } + + +def scientific_community_inclusion(record, cern_scientific_community_uuid): + """Checks if the record is included in the CERN Scientific community.""" + assert str(cern_scientific_community_uuid) in record._record.parent.communities.ids + assert cern_scientific_community_uuid != record._record.parent.communities.default diff --git a/tests/cds-rdm/test_hr_migration.py b/tests/cds-rdm/test_hr_migration.py index f39667ac..3ffd71f8 100644 --- a/tests/cds-rdm/test_hr_migration.py +++ b/tests/cds-rdm/test_hr_migration.py @@ -6,6 +6,7 @@ # the terms of the MIT License; see LICENSE file for more details. """Tests suites.""" + import json from pathlib import Path diff --git a/tests/cds-rdm/test_scientific_community.py b/tests/cds-rdm/test_scientific_community.py new file mode 100644 index 00000000..186d2b12 --- /dev/null +++ b/tests/cds-rdm/test_scientific_community.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2026 CERN. +# +# CDS-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. + +"""Tests for auto-inclusion in the CERN Research community.""" + +from unittest.mock import MagicMock + +import pytest + +from cds_migrator_kit.errors import MissingConfiguration +from cds_migrator_kit.rdm.records.transform.config import CDS_CERN_SCIENTIFIC_RESOURCE_TYPES +from cds_migrator_kit.rdm.records.transform.transform import CDSToRDMRecordTransform + + +def _test_record( + access="public", + resource_type="publication-preprint", + communities=[], + recid="123456", +): + """Build a minimal CDSToRDMRecordEntry.transform() output for community tests.""" + metadata = { + "title": "Test record", + "publication_date": "2020-01-01", + } + if resource_type is not None: + metadata["resource_type"] = {"id": resource_type} + + return { + "recid": recid, + "access": access, + "communities": communities, + "json": { + "files": {"enabled": False}, + "metadata": metadata, + }, + } + + +@pytest.fixture +def transform(tmp_path, community): + """Transform instance with a collection community configured.""" + return CDSToRDMRecordTransform( + files_dump_dir=tmp_path, + missing_users=tmp_path, + communities_ids=[str(community.id)], + migration_logger=MagicMock(), + ) + + +class TestCommunitiesIds: + """Test CDSToRDMRecordTransform._communities_ids().""" + + def test_adds_scientific_community_for_public_research_test_record( + self, transform, community, cern_scientific_community + ): + """Public research records are included in the CERN Scientific community.""" + result = transform._communities_ids({}, _test_record()) + + assert result == { + "ids": [str(community.id), str(cern_scientific_community.id)], + "default": str(community.id), + } + + def test_keep_collection_community_as_default( + self, transform, community, cern_scientific_community + ): + """Collection community remains the default when CERN Scientific community is added.""" + result = transform._communities_ids( + {}, + _test_record(communities=["test-community"]), + ) + + assert result["default"] == str(community.id) + assert result["ids"] == [ + str(community.id), + "test-community", + str(cern_scientific_community.id), + ] + + @pytest.mark.parametrize("resource_type", CDS_CERN_SCIENTIFIC_RESOURCE_TYPES) + def test_research_resource_types( + self, transform, cern_scientific_community, resource_type + ): + """All configured public research resource types trigger inclusion.""" + result = transform._communities_ids( + {}, _test_record(resource_type=resource_type) + ) + + assert str(cern_scientific_community.id) in result["ids"] + assert cern_scientific_community.id != result["default"] + + def test_skip_restricted_test_record( + self, transform, community, cern_scientific_community + ): + """Restricted records are not included in the CERN Research community.""" + result = transform._communities_ids({}, _test_record(access="restricted")) + + assert result == { + "ids": [str(community.id)], + "default": str(community.id), + } + + def test_skip_non_research_resource_type( + self, transform, community, cern_scientific_community + ): + """Non-research resource types are not included in the CERN Scientific community.""" + result = transform._communities_ids({}, _test_record(resource_type="other")) + + assert result == { + "ids": [str(community.id)], + "default": str(community.id), + } + + def test_skip_when_stream_is_restricted( + self, tmp_path, community, cern_scientific_community + ): + """Records on restricted migration streams are not included in the CERN Scientific community.""" + transform = CDSToRDMRecordTransform( + files_dump_dir=tmp_path, + missing_users=tmp_path, + communities_ids=[str(community.id)], + restricted=True, + migration_logger=MagicMock(), + ) + + result = transform._communities_ids({}, _test_record()) + + assert result == { + "ids": [str(community.id)], + "default": str(community.id), + } + + def test_raise_when_cern_scientific_community_not_configured( + self, test_app, transform, community + ): + """No CERN Scientific community is added when config is unset.""" + test_app.config["CDS_CERN_SCIENTIFIC_COMMUNITY_ID"] = None + + with pytest.raises(MissingConfiguration): + transform._communities_ids({}, _test_record())