Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make DOI optional and add CLI command for DOI minting #1979

Merged
merged 2 commits into from
Nov 19, 2024
Merged
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
34 changes: 26 additions & 8 deletions cds/modules/deposit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"""Deposit API."""


import idutils
import datetime
import os
import re
Expand Down Expand Up @@ -52,7 +53,7 @@
PIDInvalidAction,
ResolverError,
)
from invenio_pidstore.models import PersistentIdentifier
from invenio_pidstore.models import PersistentIdentifier, PIDStatus
from invenio_pidstore.resolver import Resolver
from invenio_records_files.models import RecordsBuckets
from invenio_records_files.utils import sorted_files_from_bucket
Expand All @@ -73,10 +74,11 @@
CDSRecord,
CDSVideosFilesIterator,
)
from ..records.minters import doi_minter, is_local_doi, report_number_minter
from ..records.minters import cds_doi_generator, is_local_doi, report_number_minter
from ..records.resolver import record_resolver
from ..records.utils import is_record, lowercase_value
from ..records.validators import PartialDraft4Validator
from ..records.permissions import is_public
from .errors import DiscardConflict
from .resolver import get_video_pid

Expand Down Expand Up @@ -899,12 +901,6 @@ def _publish_edited(self):
# dump again renamed subtitles
self["_files"] = self.files.dumps()

from cds.modules.records.permissions import is_public

if is_public(self, "read"):
# Mint the doi if necessary
doi_minter(record_uuid=self.id, data=self)

return super(Video, self)._publish_edited()

@mark_as_action
Expand Down Expand Up @@ -1091,6 +1087,28 @@ def _create_tags(self):
return


def mint_doi(self):
"""Mint DOI."""
assert self.has_record()
assert not self.has_minted_doi(), "DOI already exists for this video."
assert is_public(self, "read"), "Record is not public and cannot mint a DOI."

doi = cds_doi_generator(self["recid"])
# Make sure it's a proper DOI
assert idutils.is_doi(doi)
zubeydecivelek marked this conversation as resolved.
Show resolved Hide resolved

self["doi"] = doi
PersistentIdentifier.create(
"doi",
doi,
pid_provider="datacite",
object_type="rec",
object_uuid=self.id,
status=PIDStatus.RESERVED,
)
return self


project_resolver = Resolver(
pid_type="depid",
object_type="rec",
Expand Down
2 changes: 2 additions & 0 deletions cds/modules/fixtures/data/pages/faq.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ <h2 id="cds-videos">CDS Videos</h2>
</p><p>
<strong><em>Please insert the list of e-mails and e-groups, one after the other, with &quot;@cern.ch&quot; at the end.</em></strong>
</p>
<p><a href="#how-to-get-doi" name="how-to-get-doi" class="cds-anchor"><strong>How to request a DOI for a video?</strong></a></p>
<p>Videos are not automatically assigned a DOI. However, if you would like to obtain a DOI for your video, you can request one by creating a support ticket. Please use the following <a href="https://cern.service-now.com/service-portal/?id=service_element&name=MultiMedia-Service">link</a> to submit your request.</p>
<p><a href="#how-to-share" name="how-to-share" class="cds-anchor"><strong>How can I share my project with some colleagues?</strong></a></p>
<p>If you want to share the draft(s) video(s) with some colleagues, before publishing, to collaborate at the same time, edit the videos' project and use the &quot;Project editors&quot; tab: input the list of e-mail and e-groups (with &quot;@cern.ch&quot;) in the &quot;Grant access to the project&quot; field as restricting access to a video.</p>
<p><a href="#how-to-add-subtitles" name="how-to-add-subtitles" class="cds-anchor"><strong>How can I upload subtitles for my video?</strong></a></p>
Expand Down
38 changes: 37 additions & 1 deletion cds/modules/maintenance/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from invenio_files_rest.models import ObjectVersion, ObjectVersionTag
from invenio_records_files.models import RecordsBuckets

from cds.modules.deposit.api import Video
from cds.modules.flows.deposit import index_deposit_project
from cds.modules.flows.models import FlowMetadata
from cds.modules.flows.tasks import ExtractFramesTask
Expand All @@ -38,13 +37,50 @@
create_subformat,
)
from cds.modules.records.api import CDSVideosFilesIterator
from cds.modules.deposit.api import Video
from cds.modules.records.resolver import record_resolver
from cds.modules.deposit.tasks import datacite_register
from invenio_db import db
from invenio_indexer.api import RecordIndexer
from datacite.errors import DataCiteError



def abort_if_false(ctx, param, value):
if not value:
ctx.abort()

@click.command()
@click.option("--recid", "recid", help="ID of the video record", default=None, required=True)
@with_appcontext
def create_doi(recid):
"""Mints the DOI for a video record."""
# Get the video object with recid
_, record = record_resolver.resolve(recid)
depid = record.depid
video_deposit = Video.get_record(depid.object_uuid)

try:
# Mint the doi and publish
edit_record = video_deposit.edit().mint_doi().publish().commit()
# Save changes
db.session.commit()

# Index the record again
_, record_video = edit_record.fetch_published()
zubeydecivelek marked this conversation as resolved.
Show resolved Hide resolved
RecordIndexer().index(record_video)
click.echo(f"DOI minted, and indexed successfully for record '{recid}'")

# Register the doi to datacite
datacite_register(recid, str(record_video.id))

except DataCiteError as dexc:
raise ClickException(f"Failed to register DOI on datacite for the video record '{recid}': {str(dexc)}")
except Exception as exc:
db.session.rollback()
raise ClickException(f"Failed to mint DOI for the video record '{recid}': {str(exc)}")



@click.group()
def subformats():
Expand Down
10 changes: 0 additions & 10 deletions cds/modules/records/minters.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,6 @@ def cds_record_minter(record_uuid, data):
"""Mint record identifiers."""
provider = _rec_minter(record_uuid, data)

from cds.modules.deposit.api import Project

from .permissions import is_public

project_schema = current_jsonschemas.path_to_url(Project._schema)

# We shouldn't mint the DOI for the project (CDS#996)
if data.get("$schema") != project_schema and is_public(data, "read"):
doi_minter(record_uuid, data)

return provider.pid


Expand Down
22 changes: 10 additions & 12 deletions tests/unit/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import pytest
from invenio_pidstore.models import PersistentIdentifier, PIDStatus

from cds.modules.records.minters import cds_record_minter, is_local_doi
from cds.modules.records.minters import cds_record_minter, doi_minter, is_local_doi


def test_recid_provider(db):
Expand All @@ -57,14 +57,9 @@ def test_recid_provider(db):
object_uuid=uuid,
status=PIDStatus.REGISTERED,
)
pid_create.assert_any_call(
"doi",
"10.0000/videos.1",
object_type="rec",
object_uuid=uuid,
pid_provider="datacite",
status=PIDStatus.RESERVED,
)
# Verify the video has no DOI after publishing
assert "doi" not in data



@pytest.mark.parametrize(
Expand All @@ -80,6 +75,7 @@ def test_doi_minting(db, doi_in, doi_out):
rec_uuid = uuid4()
data = dict(doi=doi_in)
cds_record_minter(rec_uuid, data)
doi_minter(rec_uuid, data)
db.session.commit()

pid = PersistentIdentifier.get("doi", doi_out)
Expand All @@ -99,7 +95,7 @@ def test_invalid_doi(db, doi):
uuid = uuid4()
data = dict(doi=doi)
with pytest.raises(AssertionError):
cds_record_minter(uuid, data)
doi_minter(uuid, data)


def test_no_doi_minted_for_projects(db, api_project):
Expand All @@ -111,6 +107,9 @@ def test_no_doi_minted_for_projects(db, api_project):
# Project shouldn't have a DOI
assert project.get("doi") is None
cds_record_minter(uuid2, video_1)
# Video shouldn't have a DOI
assert "doi" not in video_1
doi_minter(uuid2, video_1)
# Video should have a DOI
assert video_1.get("doi")

Expand All @@ -122,7 +121,7 @@ def test_recid_provider_exception(db):


def test_minting_recid(db):
"""Test reminting doi for published record."""
"""Test reminting recid for published record."""
zubeydecivelek marked this conversation as resolved.
Show resolved Hide resolved
data = dict()
# Assert registration of recid.
rec_uuid = uuid4()
Expand All @@ -131,7 +130,6 @@ def test_minting_recid(db):
assert pid.pid_value == "1"
assert pid.status == PIDStatus.REGISTERED
assert pid.object_uuid == rec_uuid
assert data["doi"] == "10.0000/videos.1"
with pytest.raises(AssertionError):
cds_record_minter(rec_uuid, data)

Expand Down
95 changes: 78 additions & 17 deletions tests/unit/test_video_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
from time import sleep

import mock
from cds.modules.maintenance.cli import create_doi
from click.testing import CliRunner

import pytest
from celery.exceptions import Retry
from flask import url_for
Expand Down Expand Up @@ -90,22 +93,18 @@ def test_video_publish_registering_the_datacite(
sender=api_app, action="publish", deposit=video_1
)

assert datacite_mock.called is True
assert datacite_mock().metadata_post.call_count == 1
datacite_mock().doi_post.assert_called_once_with(
"10.0000/videos.1", "https://videos.cern.ch/record/1"
)
# [[ CONFIRM THERE'S NO DOI ]]
assert datacite_mock.called is False
assert datacite_mock().metadata_post.call_count == 0
datacite_mock().doi_post.assert_not_called()

# [[ UPDATE DATACITE ]]
# [[ ENSURE NO UPDATE IN DATACITE ]]
datacite_register_after_publish(
sender=api_app, action="publish", deposit=video_1
)

assert datacite_mock.called is True
assert datacite_mock().metadata_post.call_count == 2
datacite_mock().doi_post.assert_called_with(
"10.0000/videos.1", "https://videos.cern.ch/record/1"
)
assert datacite_mock().metadata_post.call_count == 0
datacite_mock().doi_post.assert_not_called()


@mock.patch("invenio_pidstore.providers.datacite.DataCiteMDSClient")
Expand Down Expand Up @@ -151,12 +150,10 @@ def test_video_publish_registering_the_datacite_if_fail(
):
datacite_register.s(pid_value="1", record_uuid=str(video_1.id)).apply()

assert datacite_mock.called is True
assert datacite_mock().metadata_post.call_count == 1
datacite_mock().doi_post.assert_called_once_with(
"10.0000/videos.1", "https://videos.cern.ch/record/1"
)
assert datacite_mock.call_count == 3
# [[ CONFIRM THERE'S NO DOI ]]
assert datacite_mock.called is False
assert datacite_mock().metadata_post.call_count == 0
datacite_mock().doi_post.assert_not_called()


@mock.patch("invenio_pidstore.providers.datacite.DataCiteMDSClient")
Expand Down Expand Up @@ -395,6 +392,9 @@ def test_video_publish_edit_publish_again(
# [[ PUBLISH VIDEO ]]
_deposit_publish(client, json_headers, video_1["_deposit"]["id"])

# [[ MINT DOI TO VIDEO ]]
video_1 = deposit_video_resolver(video_1_depid)
video_1.edit().mint_doi().publish().commit()
datacite_register.s(pid_value="123", record_uuid=str(video_1.id)).apply()

# [[ EDIT VIDEO ]]
Expand Down Expand Up @@ -528,6 +528,67 @@ def test_record_video_links(
}


@mock.patch("invenio_pidstore.providers.datacite.DataCiteMDSClient")
def test_mint_doi_with_cli(
Copy link
Contributor

Choose a reason for hiding this comment

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

Really nice and clear test :)

datacite_mock,
api_app,
users,
location,
json_headers,
json_partial_project_headers,
json_partial_video_headers,
deposit_metadata,
video_deposit_metadata,
project_deposit_metadata,
):
"""Test video publish without DOI, then mint DOI using CLI."""
api_app.config["DEPOSIT_DATACITE_MINTING_ENABLED"] = True

with api_app.test_client() as client:
# Log in as the first user
login_user(User.query.get(users[0]))

# Create a new project
project_dict = _create_new_project(
client, json_partial_project_headers, project_deposit_metadata
)

# Add a new empty video
video_1_dict = _add_video_info_to_project(
client, json_partial_video_headers, project_dict, video_deposit_metadata
)

video_1_depid = video_1_dict["metadata"]["_deposit"]["id"]
video_1 = deposit_video_resolver(video_1_depid)
prepare_videos_for_publish([video_1])

# Publish the video
video_1.publish()

# Verify the video has no DOI after publishing
assert "doi" not in video_1

# Use the CLI command to mint the DOI
recid = video_1['_deposit']['pid']['value']
runner = CliRunner()
result = runner.invoke(create_doi, ["--recid", recid])

assert result.exit_code == 0, f"CLI command failed: {result.output}"

# Fetch the updated record
_, updated_video = video_1.fetch_published()

# Verify that the DOI was minted successfully
doi = updated_video.get("doi")
assert doi is not None, "DOI was not minted"

# Check that the DOI was registered with DataCite
assert datacite_mock.called is True
datacite_mock().doi_post.assert_called_once_with(
doi, f"https://videos.cern.ch/record/{recid}"
)


def _deposit_edit(client, json_headers, id):
"""Post action to edit deposit."""
res = client.post(
Expand Down
Loading