Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions cds_migrator_kit/rdm/records/load/entities/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""Creates, versions, and publishes a single RDM record from a ``RecordEntry``."""

import os
from copy import deepcopy
from typing import Dict

import arrow
Expand Down Expand Up @@ -195,6 +196,40 @@ def assign_rep_numbers(self, draft):
f"Report number {report_number} already exists."
)

def _cds_doi(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general comment - I am wondering why did you decide to do this on load phase?

"""Return the CDS DataCite DOI dict, or ``None``."""
doi = (self.record_entry.body.get("pids") or {}).get("doi")
DATACITE_PREFIX = current_app.config["DATACITE_PREFIX"]
if doi and doi["identifier"].startswith(DATACITE_PREFIX):
return doi
return None

def _create_payload(self, versions):
"""Return create() body, without CDS DOI when multiple versions exist."""
if not (self._cds_doi() and len(versions) > 1):
return self.record_entry.body
data = deepcopy(self.record_entry.body)
pids = dict(data.get("pids") or {})
pids.pop("doi", None)
if pids:
data["pids"] = pids
else:
data.pop("pids", None)
return data

def _put_cds_doi_on_latest_draft(self, identity, draft, versions, version, uow):
"""Attach the CDS DOI to the latest version draft when needed."""
doi = self._cds_doi()
if not (doi and len(versions) > 1 and version == max(versions)):
return draft
draft_dict = draft.to_dict()
pids = dict(draft_dict.get("pids") or {})
pids["doi"] = doi
draft_dict["pids"] = pids
return current_rdm_records_service.update_draft(
identity, draft["id"], data=draft_dict, uow=uow
)

def pre_publish(self, identity, versions, version, draft, uow):
"""Create (or version) and process a draft before publish."""
files = versions[version]["files"]
Expand All @@ -206,7 +241,7 @@ def pre_publish(self, identity, versions, version, draft, uow):
# we decided to skip it and act normal
try:
draft = current_rdm_records_service.create(
identity, data=self.record_entry.body, uow=uow
identity, data=self._create_payload(versions), uow=uow
)
self.assign_rep_numbers(draft)
except (UniqueViolation, IntegrityError) as e:
Expand Down Expand Up @@ -248,6 +283,9 @@ def pre_publish(self, identity, versions, version, draft, uow):
identity, draft["id"], data=missing_data, uow=uow
)

draft = self._put_cds_doi_on_latest_draft(
identity, draft, versions, version, uow
)
self.load_access(draft, access, uow)
self.load_files(draft, files, uow)

Expand All @@ -257,7 +295,10 @@ def after_publish_update_dois(self, identity, record, uow):
"""Update migrated DOIs post publish."""
if not self.is_final_record:
return
migrated_pids = self.record_entry.body["pids"]
# Skip versions that do not carry the DOI (non-latest CDS DOI versions).
if "doi" not in (record.to_dict().get("pids") or {}):
return
migrated_pids = self.record_entry.body.get("pids") or {}
for pid_type, identifier in migrated_pids.items():
if pid_type == "doi":
# If a DOI was already minted from legacy then on publish the datacite
Expand Down
108 changes: 105 additions & 3 deletions tests/cds-rdm/test_transform_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pytest

from cds_migrator_kit.errors import ManualImportRequired
from cds_migrator_kit.rdm.records.load.entities.record import RecordLoad
from cds_migrator_kit.rdm.records.transform.transform import CDSToRDMRecordTransform


Expand Down Expand Up @@ -76,11 +77,19 @@ def _record(pids=None):
return SimpleNamespace(access_status="public", body=body)


CDS_DOI = {
"identifier": "10.17181/cds.12345",
"provider": "datacite",
}
EXTERNAL_DOI = {
"identifier": "10.1234/external",
"provider": "external",
}


def _external_doi_record():
"""A record double whose DOI provider is "external" (not our prefix)."""
return _record(
pids={"doi": {"identifier": "10.1234/external", "provider": "external"}}
)
return _record(pids={"doi": EXTERNAL_DOI})


@pytest.fixture
Expand Down Expand Up @@ -396,3 +405,96 @@ def test_versions_restriction_lifted_on_only_file_becomes_public(transform):
"access_obj": {"record": "public", "files": "public"}
}
assert versions[2]["files"]["A.pdf"]["version"] == 2


def _record_load(pids):
"""Build a RecordLoad with only the record body needed for DOI helpers."""
return RecordLoad(
record_entry=SimpleNamespace(
body={"pids": pids, "metadata": {"publication_date": "2020-01-01"}},
recid="123",
),
record_parent=MagicMock(),
migration_logger=MagicMock(),
)


class _DraftItem(dict):
"""Minimal stand-in for an RDM draft item (supports ``[]`` and ``to_dict``)."""

def to_dict(self):
return dict(self)


def _patch_datacite_prefix(monkeypatch, prefix="10.17181"):
"""Point load helpers at a fixed DATACITE_PREFIX without a full app."""
monkeypatch.setattr(
"cds_migrator_kit.rdm.records.load.entities.record.current_app",
SimpleNamespace(config={"DATACITE_PREFIX": prefix}),
)


def test_cds_doi_uses_datacite_prefix_not_provider(monkeypatch):
"""CDS DOI detection follows DATACITE_PREFIX, not the provider label."""
_patch_datacite_prefix(monkeypatch)
mistyped_provider = {
"identifier": "10.17181/cds.12345",
"provider": "external",
}
wrong_prefix = {
"identifier": "10.1234/external",
"provider": "datacite",
}
assert _record_load({"doi": mistyped_provider})._cds_doi() == mistyped_provider
assert _record_load({"doi": wrong_prefix})._cds_doi() is None
assert _record_load({"doi": CDS_DOI})._cds_doi() == CDS_DOI


def test_create_payload_keeps_cds_doi_for_single_version(monkeypatch):
"""Single-version CDS DOI records keep the DOI on create."""
_patch_datacite_prefix(monkeypatch)
payload = _record_load({"doi": CDS_DOI})._create_payload(versions={1: {}})
assert payload["pids"]["doi"] == CDS_DOI


def test_create_payload_strips_cds_doi_when_multiple_versions(monkeypatch):
"""Multi-version CDS DOI records strip the DOI from the first create."""
_patch_datacite_prefix(monkeypatch)
payload = _record_load({"doi": CDS_DOI})._create_payload(versions={1: {}, 2: {}})
assert "doi" not in payload.get("pids", {})


def test_put_cds_doi_only_on_latest_of_multiple_versions(monkeypatch):
"""CDS DOI is attached only when publishing the latest version."""
_patch_datacite_prefix(monkeypatch)
load = _record_load({"doi": CDS_DOI})
draft = _DraftItem({"id": "draft-id", "pids": {}, "metadata": {}})
updated_draft = _DraftItem(
{"id": "draft-id", "pids": {"doi": CDS_DOI}, "metadata": {}}
)

identity = MagicMock()
uow = MagicMock()
service = MagicMock()
service.update_draft.return_value = updated_draft
monkeypatch.setattr(
"cds_migrator_kit.rdm.records.load.entities.record.current_rdm_records_service",
service,
)

out = load._put_cds_doi_on_latest_draft(
identity, draft, versions={1: {}, 2: {}}, version=1, uow=uow
)
assert out is draft
service.update_draft.assert_not_called()

out = load._put_cds_doi_on_latest_draft(
identity, draft, versions={1: {}, 2: {}}, version=2, uow=uow
)
assert out is updated_draft
service.update_draft.assert_called_once_with(
identity,
"draft-id",
data={"id": "draft-id", "pids": {"doi": CDS_DOI}, "metadata": {}},
uow=uow,
)
Loading