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/records/transform/config.py b/cds_migrator_kit/rdm/records/transform/config.py index 5d0fad6e..915ed1f9 100644 --- a/cds_migrator_kit/rdm/records/transform/config.py +++ b/cds_migrator_kit/rdm/records/transform/config.py @@ -158,6 +158,24 @@ FILE_SUBFORMATS_TO_DROP = ["pdfa", "unstamped"] + +# Public research publication resource types that are auto-included in the CERN Research community. +CERN_SCIENTIFIC_RESOURCE_TYPES = { + "publication-dissertation", # Already included by the migrator for thesis records. + "publication-book", + "publication-section", + "publication-conferencepaper", + "publication-conferenceproceeding", + "publication-conferencenote", + "publication-journal", + "publication-article", + "publication-preprint", + "publication-report", + "publication-technicalnote", + "publication-note", + "publication", +} + # Legacy experiment names remapped to vocabulary ids before lookup EXPERIMENT_ALIASES = { "t2k": "re13", diff --git a/cds_migrator_kit/rdm/records/transform/transform.py b/cds_migrator_kit/rdm/records/transform/transform.py index 1750b597..149b3499 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,6 +32,7 @@ from cds_migrator_kit.errors import ( ManualImportRequired, + MissingConfiguration, MissingRequiredField, MultipleModelsMatched, RecordFlaggedCuration, @@ -43,6 +44,7 @@ VOCABULARIES_NAMES_SCHEMES, ) from cds_migrator_kit.rdm.records.transform.config import ( + CERN_SCIENTIFIC_RESOURCE_TYPES, EXPERIMENT_ALIASES, FILE_SUBFORMATS_TO_DROP, IDENTIFIERS_SCHEMES_TO_DROP, @@ -857,9 +859,42 @@ def __init__( self.db_state = {"affiliations": CDSMigrationAffiliationMapping} super().__init__(workers, throw) + def _should_add_scientific_community(self, entry, record): + """ + Determine if the scientific community should be added to the record. + + The scientific community is added if the following conditions are met: + - The record is public + - The files are public + - The record has a resource type in the CERN Scientific resource types + """ + if self.restricted or record.get("access") != "public": + return False + if any(file.get("status") for file in entry.get("files", [])): + return False + resource_type_id = ( + record.get("json", {}) + .get("metadata", {}) + .get("resource_type", {}) + .get("id") + ) + return resource_type_id in 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(entry, record): + if scientific_community not in communities: + communities.append(scientific_community) + if communities: return {"ids": communities, "default": self.communities_ids[0]} return {} @@ -913,6 +948,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 dd169fc3..f370c49e 100644 --- a/tests/cds-rdm/conftest.py +++ b/tests/cds-rdm/conftest.py @@ -278,7 +278,7 @@ def running_app( @pytest.fixture -def test_app(running_app): +def test_app(running_app, cern_scientific_community): """Get current app.""" running_app.app.config["RDM_PERSISTENT_IDENTIFIERS"]["doi"]["required"] = False running_app.app.config["RDM_PARENT_PERSISTENT_IDENTIFIERS"]["doi"][ @@ -1703,6 +1703,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_bulletin_issue.py b/tests/cds-rdm/test_bulletin_issue.py index 88cd0bdf..12c2d0f9 100644 --- a/tests/cds-rdm/test_bulletin_issue.py +++ b/tests/cds-rdm/test_bulletin_issue.py @@ -80,8 +80,3 @@ def test_bulletin_issue( ) if record["legacy_recid"] == "2234683": parent_related_identifier(loaded_rec) - - -# FAILED tests/cds-rdm/test_access_permissions.py::test_access_permissions - assert 2 == 3 -# FAILED tests/cds-rdm/test_bulletin_issue.py::test_bulletin_issue - AssertionError: assert [{'identifier...heme': 'cds'}] == [{'identifier...heme': 'cds'}] -# FAILED tests/cds-rdm/test_full_migration.py::test_full_migration_stream - assert 0 == 2 diff --git a/tests/cds-rdm/test_ep_approval_entry.py b/tests/cds-rdm/test_ep_approval_entry.py index 33bd9d3b..ac6374ca 100644 --- a/tests/cds-rdm/test_ep_approval_entry.py +++ b/tests/cds-rdm/test_ep_approval_entry.py @@ -14,11 +14,11 @@ import pytest from cds_migrator_kit.errors import UnexpectedValue -from cds_migrator_kit.rdm.migration_config import CDS_CERN_SCIENTIFIC_COMMUNITY_ID from cds_migrator_kit.rdm.records.load.ep_approval_entry import ( EPPHAPP_FILE_TYPE, PublicEntry, RestrictedEntry, + _cern_scientific_community_id, ) RECID = "12345" @@ -526,7 +526,7 @@ def test_public_adds_cern_scientific_community(self, app): entry, _make_approval_request(), _make_migration_logger() ).build() - assert CDS_CERN_SCIENTIFIC_COMMUNITY_ID in ( + assert _cern_scientific_community_id() in ( result["parent"]["json"]["communities"]["ids"] ) @@ -534,14 +534,14 @@ def test_public_does_not_duplicate_community(self, app): entry = _make_entry(_versions_with_epphapp()) entry["parent"]["json"]["communities"]["ids"] = [ "example-community", - CDS_CERN_SCIENTIFIC_COMMUNITY_ID, + _cern_scientific_community_id(), ] result = PublicEntry( entry, _make_approval_request(), _make_migration_logger() ).build() community_ids = result["parent"]["json"]["communities"]["ids"] - assert community_ids.count(CDS_CERN_SCIENTIFIC_COMMUNITY_ID) == 1 + assert community_ids.count(_cern_scientific_community_id()) == 1 class TestEntryImmutability: diff --git a/tests/cds-rdm/test_full_migration.py b/tests/cds-rdm/test_full_migration.py index 3df27f70..ec14c332 100644 --- a/tests/cds-rdm/test_full_migration.py +++ b/tests/cds-rdm/test_full_migration.py @@ -471,6 +471,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, ): @@ -515,6 +516,8 @@ def test_full_migration_stream( if record["legacy_recid"] == "2783104": file_restricted(loaded_rec) parent_access_fields(loaded_rec) + # Skip scientific community inclusion check since it has restricted files + continue if record["legacy_recid"] == "2046076": irregular_exp_field(loaded_rec) if record["legacy_recid"] == "2041388": @@ -526,6 +529,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() @@ -595,3 +600,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_scientific_community.py b/tests/cds-rdm/test_scientific_community.py new file mode 100644 index 00000000..f3f31e33 --- /dev/null +++ b/tests/cds-rdm/test_scientific_community.py @@ -0,0 +1,175 @@ +# -*- 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 ( + 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, + }, + } + + +def _test_entry(files_restricted=False): + """Build a minimal raw dump entry for community tests.""" + status = "restricted" if files_restricted else "" + return {"files": [{"status": status}]} + + +@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.""" + record = _test_record() + record["json"]["files"] = {"enabled": True} + result = transform._communities_ids( + _test_entry(), + 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_entry(), + _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", 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_entry(), _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_entry(), _test_record(access="restricted") + ) + + assert result == { + "ids": [str(community.id)], + "default": str(community.id), + } + + def test_skip_restricted_files( + self, transform, community, cern_scientific_community + ): + """Records with restricted files are not included in the CERN Scientific community.""" + result = transform._communities_ids( + _test_entry(files_restricted=True), _test_record() + ) + + 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_entry(), _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_entry(), _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, monkeypatch + ): + """No CERN Scientific community is added when config is unset.""" + monkeypatch.setitem(test_app.config, "CDS_CERN_SCIENTIFIC_COMMUNITY_ID", None) + + with pytest.raises(MissingConfiguration): + transform._communities_ids(_test_entry(), _test_record())