From e60349f975045aa2084654ff29615d3395d302a8 Mon Sep 17 00:00:00 2001 From: Erik Hanson Date: Fri, 6 Oct 2023 14:05:01 -0700 Subject: [PATCH] pkp/pkp-lib#9771 Move ORCID functionality into core application --- api/v1/orcid/index.php | 20 +++ classes/orcid/OrcidReview.php | 134 ++++++++++++++++++ classes/orcid/OrcidWork.php | 116 +++++++++++++++ .../orcid/actions/SendSubmissionToOrcid.php | 44 ++++++ dbscripts/xml/upgrade.xml | 1 + jobs/orcid/PublishReviewerWorkToOrcid.php | 121 ++++++++++++++++ pages/article/ArticleHandler.php | 2 + plugins/themes/default/styles/index.less | 2 + registry/emailTemplates.xml | 2 + registry/uiLocaleKeysBackend.json | 6 + 10 files changed, 448 insertions(+) create mode 100644 api/v1/orcid/index.php create mode 100644 classes/orcid/OrcidReview.php create mode 100644 classes/orcid/OrcidWork.php create mode 100644 classes/orcid/actions/SendSubmissionToOrcid.php create mode 100644 jobs/orcid/PublishReviewerWorkToOrcid.php diff --git a/api/v1/orcid/index.php b/api/v1/orcid/index.php new file mode 100644 index 00000000000..9e3bc82e2d3 --- /dev/null +++ b/api/v1/orcid/index.php @@ -0,0 +1,20 @@ +data = $this->build(); + } + + /** + * Returns ORCID review data as an associative array, ready for deposit. + */ + public function toArray(): array + { + return $this->data; + } + + /** + * Builds the internal structure for the ORCID review. + */ + private function build(): array + { + $publicationUrl = Application::get()->getDispatcher()->url( + Application::get()->getRequest(), + Application::ROUTE_PAGE, + $this->context->getPath(), + 'article', + 'view', + $this->submission->getId(), + urlLocaleForPage: '', + ); + + $submissionLocale = $this->submission->getData('locale'); + $currentPublication = $this->submission->getCurrentPublication(); + + if (empty($this->review->getData('dateCompleted')) || empty($this->context->getData('onlineIssn'))) { + return []; + } + + $reviewCompletionDate = Carbon::parse($this->review->getData('dateCompleted')); + + $orcidReview = [ + 'reviewer-role' => 'reviewer', + 'review-type' => 'review', + 'review-completion-date' => [ + 'year' => [ + 'value' => $reviewCompletionDate->format('Y') + ], + 'month' => [ + 'value' => $reviewCompletionDate->format('m') + ], + 'day' => [ + 'value' => $reviewCompletionDate->format('d') + ] + ], + 'review-group-id' => 'issn:' . $this->context->getData('onlineIssn'), + + 'convening-organization' => [ + 'name' => $this->context->getData('publisherInstitution'), + 'address' => [ + 'city' => OrcidManager::getCity($this->context), + 'country' => OrcidManager::getCountry($this->context), + + ] + ], + 'review-identifiers' => ['external-id' => [ + [ + 'external-id-type' => 'source-work-id', + 'external-id-value' => $this->review->getData('reviewRoundId'), + 'external-id-relationship' => 'part-of'] + ]] + ]; + if ($this->review->getReviewMethod() == ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN) { + $orcidReview['subject-url'] = ['value' => $publicationUrl]; + $orcidReview['review-url'] = ['value' => $publicationUrl]; + $orcidReview['subject-type'] = 'journal-article'; + $orcidReview['subject-name'] = [ + 'title' => ['value' => $this->submission->getCurrentPublication()->getLocalizedData('title') ?? ''] + ]; + + if (!empty($currentPublication->getDoi())) { + /** @var Doi $doiObject */ + $doiObject = $currentPublication->getData('doiObject'); + $externalIds = [ + 'external-id-type' => 'doi', + 'external-id-value' => $doiObject->getDoi(), + 'external-id-url' => [ + 'value' => $doiObject->getResolvingUrl(), + ], + 'external-id-relationship' => 'self' + + ]; + $orcidReview['subject-external-identifier'] = $externalIds; + } + } + + $allTitles = $currentPublication->getData('title'); + foreach ($allTitles as $locale => $title) { + if ($locale !== $submissionLocale) { + $orcidReview['subject-name']['translated-title'] = ['value' => $title, 'language-code' => LocaleConversion::getIso1FromLocale($locale)]; + } + } + + return $orcidReview; + } +} diff --git a/classes/orcid/OrcidWork.php b/classes/orcid/OrcidWork.php new file mode 100644 index 00000000000..74b27469a3e --- /dev/null +++ b/classes/orcid/OrcidWork.php @@ -0,0 +1,116 @@ +publication, $this->context, $this->authors); + } + + /** + * @inheritdoc + */ + protected function getAppPubIdExternalIds(PubIdPlugin $plugin): array + { + $ids = []; + + $pubIdType = $plugin->getPubIdType(); + $pubId = $this->issue?->getStoredPubId($pubIdType); + if ($pubId) { + $ids[] = [ + 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType], + 'external-id-value' => $pubId, + 'external-id-url' => [ + 'value' => $plugin->getResolvingURL($this->context->getId(), $pubId) + ], + 'external-id-relationship' => 'part-of' + ]; + } + + return $ids; + } + + /** + * @inheritdoc + */ + protected function getAppDoiExternalIds(): array + { + $ids = []; + + $issueDoiObject = $this->issue->getData('doiObject'); + if ($issueDoiObject) { + $ids[] = [ + 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID['doi'], + 'external-id-value' => $issueDoiObject->getData('doi'), + 'external-id-url' => [ + 'value' => $issueDoiObject->getResolvingUrl() + ], + 'external-id-relationship' => 'part-of' + ]; + } + + return $ids; + } + + /** + * @inheritDoc + */ + protected function getOrcidPublicationType(): string + { + return 'journal-article'; + } + + /** + * @inheritdoc + */ + protected function getBibtexCitation(Submission $submission): string + { + $request = Application::get()->getRequest(); + try { + PluginRegistry::loadCategory('generic'); + /** @var CitationStyleLanguagePlugin $citationPlugin */ + $citationPlugin = PluginRegistry::getPlugin('generic', 'citationstylelanguageplugin'); + return trim( + strip_tags( + $citationPlugin->getCitation( + $request, + $submission, + 'bibtex', + $this->issue, + $this->publication + ) + ) + ); + } catch (\Exception $exception) { + return ''; + } + } +} diff --git a/classes/orcid/actions/SendSubmissionToOrcid.php b/classes/orcid/actions/SendSubmissionToOrcid.php new file mode 100644 index 00000000000..bb7de09e32e --- /dev/null +++ b/classes/orcid/actions/SendSubmissionToOrcid.php @@ -0,0 +1,44 @@ +publication->getData('issueId'); + if (isset($issueId)) { + $issue = Repo::issue()->get($issueId); + } + + return new OrcidWork($this->publication, $this->context, $authors, $issue ?? null); + } + + /** + * @inheritDoc + */ + protected function canDepositSubmission(): bool + { + return true; + } +} diff --git a/dbscripts/xml/upgrade.xml b/dbscripts/xml/upgrade.xml index c662a2df355..ffd6efbbaa3 100644 --- a/dbscripts/xml/upgrade.xml +++ b/dbscripts/xml/upgrade.xml @@ -133,6 +133,7 @@ + diff --git a/jobs/orcid/PublishReviewerWorkToOrcid.php b/jobs/orcid/PublishReviewerWorkToOrcid.php new file mode 100644 index 00000000000..d5b1ae46edf --- /dev/null +++ b/jobs/orcid/PublishReviewerWorkToOrcid.php @@ -0,0 +1,121 @@ +fail('Application is set to sandbox mode and will not interact with the ORCID service'); + return; + } + + if (!OrcidManager::isMemberApiEnabled($this->context)) { + return; + } + + if (!OrcidManager::getCity($this->context) || !OrcidManager::getCountry($this->context)) { + return; + } + + $reviewer = Repo::user()->get($this->reviewAssignment->getData('reviewerId')); + + if ($reviewer->getOrcid() && $reviewer->getData('orcidAccessToken')) { + $orcidAccessExpiresOn = Carbon::parse($reviewer->getData('orcidAccessExpiresOn')); + if ($orcidAccessExpiresOn->isFuture()) { + # Extract only the ORCID from the stored ORCID uri + $orcid = basename(parse_url($reviewer->getOrcid(), PHP_URL_PATH)); + + $orcidReview = new OrcidReview($this->submission, $this->reviewAssignment, $this->context); + + $uri = OrcidManager::getApiPath($this->context) . OrcidManager::ORCID_API_VERSION_URL . $orcid . '/' . OrcidManager::ORCID_REVIEW_URL; + $method = 'POST'; + if ($putCode = $reviewer->getData('orcidReviewPutCode')) { + $uri .= '/' . $putCode; + $method = 'PUT'; + $orcidReview['put-code'] = $putCode; + } + $headers = [ + 'Content-Type' => ' application/vnd.orcid+json; qs=4', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $reviewer->getData('orcidAccessToken') + ]; + $httpClient = Application::get()->getHttpClient(); + + try { + $response = $httpClient->request( + $method, + $uri, + [ + 'headers' => $headers, + 'json' => $orcidReview->toArray(), + ] + ); + + $httpStatus = $response->getStatusCode(); + OrcidManager::logInfo("Response status: {$httpStatus}"); + $responseHeaders = $response->getHeaders(); + switch ($httpStatus) { + case 200: + OrcidManager::logInfo("Review updated in profile, putCode: {$putCode}"); + break; + case 201: + $location = $responseHeaders['Location'][0]; + // Extract the ORCID work put code for updates/deletion. + $putCode = basename(parse_url($location, PHP_URL_PATH)); + $reviewer->setData('orcidReviewPutCode', $putCode); + Repo::user()->edit($reviewer, ['orcidReviewPutCode']); + OrcidManager::logInfo("Review added to profile, putCode: {$putCode}"); + break; + default: + OrcidManager::logError("Unexpected status {$httpStatus} response, body: " . json_encode($responseHeaders)); + } + } catch (ClientException $exception) { + $reason = $exception->getResponse()->getBody(); + OrcidManager::logError("Publication fail: {$reason}"); + + $this->fail($exception); + } + } + } + } +} diff --git a/pages/article/ArticleHandler.php b/pages/article/ArticleHandler.php index 18de5d32962..57ae0b93020 100644 --- a/pages/article/ArticleHandler.php +++ b/pages/article/ArticleHandler.php @@ -34,6 +34,7 @@ use PKP\core\PKPApplication; use PKP\core\PKPJwt as JWT; use PKP\db\DAORegistry; +use PKP\orcid\OrcidManager; use PKP\plugins\Hook; use PKP\plugins\PluginRegistry; use PKP\security\authorization\ContextRequiredPolicy; @@ -307,6 +308,7 @@ public function view($args, $request) 'copyrightYear' => $publication->getData('copyrightYear'), 'pubIdPlugins' => PluginRegistry::loadCategory('pubIds', true), 'keywords' => $publication->getData('keywords'), + 'orcidIcon' => OrcidManager::getIcon(), ]); // Fetch and assign the galley to the template diff --git a/plugins/themes/default/styles/index.less b/plugins/themes/default/styles/index.less index 52eea2223f7..5b82741f955 100644 --- a/plugins/themes/default/styles/index.less +++ b/plugins/themes/default/styles/index.less @@ -20,6 +20,8 @@ @import "../../../../lib/pkp/styles/variables.less"; @import "../../../../lib/pkp/styles/utils.less"; @import "../../../../lib/pkp/styles/helpers.less"; +// General ORCID styles +@import '../../../../lib/pkp/styles/orcid.less'; // Styles unique to this theme @import "variables.less"; diff --git a/registry/emailTemplates.xml b/registry/emailTemplates.xml index 33a38623f5c..5fac37ce923 100644 --- a/registry/emailTemplates.xml +++ b/registry/emailTemplates.xml @@ -76,4 +76,6 @@ + + diff --git a/registry/uiLocaleKeysBackend.json b/registry/uiLocaleKeysBackend.json index 3763c71436a..ce214ec322f 100644 --- a/registry/uiLocaleKeysBackend.json +++ b/registry/uiLocaleKeysBackend.json @@ -126,6 +126,12 @@ "manager.dois.update.partialFailure", "manager.dois.update.success", "navigation.backTo", + "orcid.field.authorEmailModal.message", + "orcid.field.authorEmailModal.title", + "orcid.field.deleteOrcidModal.message", + "orcid.field.deleteOrcidModal.title", + "orcid.field.verification.request", + "orcid.field.verification.requested", "publication.jats.autoCreatedMessage", "publication.jats.confirmDeleteFileButton", "publication.jats.confirmDeleteFileMessage",