From fcfc223012e8f14aab9c434d4406bdc78a0b8ce8 Mon Sep 17 00:00:00 2001 From: Erik Hanson Date: Wed, 8 May 2024 11:10:42 -0700 Subject: [PATCH] WIP: Add OPS compatibility --- classes/orcid/OrcidWork.php | 311 ++++-------------- .../orcid/actions/SendSubmissionToOrcid.php | 65 +--- jobs/orcid/DepositOrcidSubmission.php | 142 -------- 3 files changed, 75 insertions(+), 443 deletions(-) delete mode 100644 jobs/orcid/DepositOrcidSubmission.php diff --git a/classes/orcid/OrcidWork.php b/classes/orcid/OrcidWork.php index accc2cdbeca..ccbf1a27914 100644 --- a/classes/orcid/OrcidWork.php +++ b/classes/orcid/OrcidWork.php @@ -14,294 +14,103 @@ namespace APP\orcid; -use APP\author\Author; use APP\core\Application; -use APP\facades\Repo; use APP\issue\Issue; -use APP\journal\Journal; use APP\plugins\generic\citationStyleLanguage\CitationStyleLanguagePlugin; +use APP\plugins\PubIdPlugin; use APP\publication\Publication; use APP\submission\Submission; -use Carbon\Carbon; use PKP\context\Context; -use PKP\i18n\LocaleConversion; +use PKP\orcid\PKPOrcidWork; use PKP\plugins\PluginRegistry; -class OrcidWork +class OrcidWork extends PKPOrcidWork { - public const PUBID_TO_ORCID_EXT_ID = ['doi' => 'doi', 'other::urn' => 'urn']; - public const USER_GROUP_TO_ORCID_ROLE = ['Author' => 'AUTHOR', 'Translator' => 'CHAIR_OR_TRANSLATOR', 'Journal manager' => 'AUTHOR']; - - private array $data = []; - public function __construct( - private Publication $publication, - private Context $context, - private array $authors, - private ?Issue $issue = null + protected Publication $publication, + protected Context $context, + protected array $authors, + protected ?Issue $issue = null ) { - $this->data = $this->build(); - } - - public function toArray(): array - { - return $this->data; + parent::__construct($this->publication, $this->context, $this->authors); } - private function build(): array + /** + * @inheritdoc + */ + protected function getAppPubIdExternalIds(PubIdPlugin $plugin): array { - $submission = Repo::submission()->get($this->publication->getData('submissionId')); - - $applicationName = Application::get()->getName(); - $bibtexCitation = ''; - - $publicationLocale = $this->publication->getData('locale'); - - $publicationUrl = Application::get()->getDispatcher()->url( - Application::get()->getRequest(), - Application::ROUTE_PAGE, - $this->context->getPath(), - 'article', - 'view', - $submission->getId(), - urlLocaleForPage: '', - ); - - $orcidWork = [ - 'title' => [ - 'title' => [ - 'value' => trim(strip_tags($this->publication->getLocalizedTitle($publicationLocale))) ?? '' + $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) ], - 'subtitle' => [ - 'value' => trim(strip_tags($this->publication->getLocalizedData('subtitle', $publicationLocale))) ?? '' - ] - ], - 'journal-title' => [ - 'value' => $this->context->getName($publicationLocale) ?? $this->context->getName($this->context->getPrimaryLocale()), - ], - 'short-description' => trim(strip_tags($this->publication->getLocalizedData('abstract', $publicationLocale))) ?? '', - - 'external-ids' => [ - 'external-id' => $this->buildOrcidExternalIds($submission, $this->publication, $this->context, $this->issue, $publicationUrl) - ], - 'publication-date' => $this->buildOrcidPublicationDate($this->publication, $this->issue), - 'url' => $publicationUrl, - 'language-code' => LocaleConversion::getIso1FromLocale($publicationLocale), - 'contributors' => [ - 'contributor' => $this->buildOrcidContributors($this->authors, $this->context, $this->publication) - ] - ]; - - if ($applicationName == 'ojs2') { - PluginRegistry::loadCategory('generic'); - $citationPlugin = PluginRegistry::getPlugin('generic', 'citationstylelanguageplugin'); - /** @var CitationStyleLanguagePlugin $citationPlugin */ - $bibtexCitation = trim(strip_tags($citationPlugin->getCitation($this->request, $submission, 'bibtex', $this->issue, $this->publication))); - $orcidWork['citation'] = [ - 'citation-type' => 'bibtex', - 'citation-value' => $bibtexCitation, + 'external-id-relationship' => 'part-of' ]; - $orcidWork['type'] = 'journal-article'; - } elseif ($applicationName == 'ops') { - $orcidWork['type'] = 'preprint'; - } - - foreach ($this->publication->getData('title') as $locale => $title) { - if ($locale !== $publicationLocale) { - $orcidWork['title']['translated-title'] = ['value' => $title, 'language-code' => LocaleConversion::getIso1FromLocale($locale)]; - } } - return $orcidWork; + return $ids; } /** - * Build the external identifiers ORCID JSON structure from article, journal and issue meta data. - * - * @see https://pub.orcid.org/v2.0/identifiers Table of valid ORCID identifier types. - * - * @param Submission $submission The Article object for which the external identifiers should be build. - * @param Publication $publication The Article object for which the external identifiers should be build. - * @param Journal $context Context the Submission is part of. - * @param Issue $issue The Issue object the Article object belongs to. - * - * @return array An associative array corresponding to ORCID external-id JSON. + * @inheritdoc */ - private function buildOrcidExternalIds(Submission $submission, Publication $publication, Context $context, Issue $issue, string $articleUrl): array + protected function getAppDoiExternalIds(): array { - $contextId = $context->getId(); - - $externalIds = []; - $pubIdPlugins = PluginRegistry::loadCategory('pubIds', true, $contextId); - // Add doi, urn, etc. for article - $articleHasStoredPubId = false; - - // Handle non-DOI pubIds - if (!empty($pubIdPlugins)) { - foreach ($pubIdPlugins as $plugin) { - if (!$plugin->getEnabled()) { - continue; - } - - $pubIdType = $plugin->getPubIdType(); - - # Add article ids - $pubId = $publication->getStoredPubId($pubIdType); - - if ($pubId) { - $externalIds[] = [ - 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType], - 'external-id-value' => $pubId, - 'external-id-url' => [ - 'value' => $plugin->getResolvingURL($contextId, $pubId) - ], - 'external-id-relationship' => 'self' - ]; - - $articleHasStoredPubId = true; - } - - # Add issue ids if they exist - $pubId = $issue->getStoredPubId($pubIdType); - if ($pubId) { - $externalIds[] = [ - 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType], - 'external-id-value' => $pubId, - 'external-id-url' => [ - 'value' => $plugin->getResolvingURL($contextId, $pubId) - ], - 'external-id-relationship' => 'part-of' - ]; - } - } - } - - // Handle DOIs - if ($context->areDoisEnabled()) { - # Add article ids - $publicationDoiObject = $publication->getData('doiObject'); - - if ($publicationDoiObject) { - $externalIds[] = [ - 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID['doi'], - 'external-id-value' => $publicationDoiObject->getData('doi'), - 'external-id-url' => [ - 'value' => $publicationDoiObject->getResolvingUrl() - ], - 'external-id-relationship' => 'self' - ]; - - $articleHasStoredPubId = true; - } - - // Add issue ids if they exist - $issueDoiObject = $issue->getData('doiObject'); - if ($issueDoiObject) { - $externalIds[] = [ - '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' - ]; - } - } - - if (!$articleHasStoredPubId) { - // No pubidplugins available or article does not have any stored pubid - // Use URL as an external-id - $externalIds[] = [ - 'external-id-type' => 'uri', - 'external-id-value' => $articleUrl, - 'external-id-relationship' => 'self' - ]; - } - - // Add journal online ISSN - // TODO What about print ISSN? - if ($context->getData('onlineIssn')) { - $externalIds[] = [ - 'external-id-type' => 'issn', - 'external-id-value' => $context->getData('onlineIssn'), + $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 $externalIds; + return $ids; } /** - * Parse issue year and publication date and use the older one of the two as - * the publication date of the ORCID work. - * - * @param null|mixed $issue - * - * @return array Associative array with year, month and day or only year + * @inheritDoc */ - private function buildOrcidPublicationDate(Publication $publication, ?Issue $issue = null): array + protected function getOrcidPublicationType(): string { - $publicationPublishDate = Carbon::parse($publication->getData('datePublished')); - - return [ - 'year' => ['value' => $publicationPublishDate->format('Y')], - 'month' => ['value' => $publicationPublishDate->format('m')], - 'day' => ['value' => $publicationPublishDate->format('d')] - ]; + return 'journal-article'; } /** - * Build associative array fitting for ORCID contributor mentions in an - * ORCID work from the supplied Authors array. - * - * @param Author[] $authors Array of Author objects - * - * @return array[] Array of associative arrays, - * one for each contributor + * @inheritdoc */ - private function buildOrcidContributors(array $authors, Context $context, Publication $publication): array + protected function getBibtexCitation(Submission $submission): string { - $contributors = []; - $first = true; - - foreach ($authors as $author) { - $contributor = [ - 'credit-name' => $author->getFullName(), - 'contributor-attributes' => [ - 'contributor-sequence' => $first ? 'first' : 'additional' - ] - ]; - - $userGroup = $author->getUserGroup(); - $role = self::USER_GROUP_TO_ORCID_ROLE[$userGroup->getName('en')]; - - if ($role) { - $contributor['contributor-attributes']['contributor-role'] = $role; - } - - if ($author->getOrcid()) { - $orcid = basename(parse_url($author->getOrcid(), PHP_URL_PATH)); - - if ($author->getData('orcidSandbox')) { - $uri = ORCID_URL_SANDBOX . $orcid; - $host = 'sandbox.orcid.org'; - } else { - $uri = $author->getOrcid(); - $host = 'orcid.org'; - } - - $contributor['contributor-orcid'] = [ - 'uri' => $uri, - 'path' => $orcid, - 'host' => $host - ]; - } - - $first = false; - - $contributors[] = $contributor; + $request = Application::get()->getRequest(); + PluginRegistry::loadCategory('generic'); + /** @var CitationStyleLanguagePlugin $citationPlugin */ + $citationPlugin = PluginRegistry::getPlugin('generic', 'citationstylelanguageplugin'); + try { + return trim( + strip_tags( + $citationPlugin->getCitation( + $request, + $submission, + 'bibtex', + $this->issue, + $this->publication + ) + ) + ); + } catch (\Exception $exception) { + return ''; } - - return $contributors; } } diff --git a/classes/orcid/actions/SendSubmissionToOrcid.php b/classes/orcid/actions/SendSubmissionToOrcid.php index 38bb22937bb..bb7de09e32e 100644 --- a/classes/orcid/actions/SendSubmissionToOrcid.php +++ b/classes/orcid/actions/SendSubmissionToOrcid.php @@ -15,65 +15,30 @@ namespace APP\orcid\actions; use APP\facades\Repo; -use APP\jobs\orcid\DepositOrcidSubmission; use APP\orcid\OrcidWork; -use APP\publication\Publication; -use Carbon\Carbon; -use PKP\context\Context; -use PKP\orcid\OrcidManager; +use PKP\orcid\actions\PKPSendSubmissionToOrcid; +use PKP\orcid\PKPOrcidWork; -class SendSubmissionToOrcid +class SendSubmissionToOrcid extends PKPSendSubmissionToOrcid { - public function __construct( - private Publication $publication, - private Context $context, - ) { - } - - public function execute(): void + /** + * @inheritDoc + */ + protected function getOrcidWork(array $authors): ?PKPOrcidWork { - if (!OrcidManager::isMemberApiEnabled($this->context)) { - // Sending to ORCID only works with the member API - return; - } - $issueId = $this->publication->getData('issueId'); if (isset($issueId)) { $issue = Repo::issue()->get($issueId); } - $authors = Repo::author() - ->getCollector() - ->filterByPublicationIds([$this->publication->getId()]) - ->getMany(); - - // Collect valid ORCID ids and access tokens from submission contributors - $authorsWithOrcid = []; - foreach ($authors as $author) { - if ($author->getOrcid() && $author->getData('orcidAccessToken')) { - $orcidAccessExpiresOn = Carbon::parse($author->getData('orcidAccessExpiresOn')); - if ($orcidAccessExpiresOn->isFuture()) { - # Extract only the ORCID from the stored ORCID uri - $orcid = basename(parse_url($author->getOrcid(), PHP_URL_PATH)); - $authorsWithOrcid[$orcid] = $author; - } else { - OrcidManager::logError("Token expired on {$orcidAccessExpiresOn} for author " . $author->getId() . ', deleting orcidAccessToken!'); - OrcidManager::removeOrcidAccessToken($author); - } - } - } - - if (empty($authorsWithOrcid)) { - OrcidManager::logInfo('No contributor with ORICD id or valid access token for submission ' . $this->publication->getData('submissionId')); - return; - } - - $orcidWork = new OrcidWork($this->publication, $this->context, $authors->toArray(), $issue ?? null); - OrcidManager::logInfo('Request body (without put-code): ' . json_encode($orcidWork->toArray())); - - foreach ($authorsWithOrcid as $orcid => $author) { - dispatch(new DepositOrcidSubmission($author, $this->context, $orcidWork->toArray(), $orcid)); - } + return new OrcidWork($this->publication, $this->context, $authors, $issue ?? null); + } + /** + * @inheritDoc + */ + protected function canDepositSubmission(): bool + { + return true; } } diff --git a/jobs/orcid/DepositOrcidSubmission.php b/jobs/orcid/DepositOrcidSubmission.php deleted file mode 100644 index b731b749652..00000000000 --- a/jobs/orcid/DepositOrcidSubmission.php +++ /dev/null @@ -1,142 +0,0 @@ -fail('Application is set to sandbox mode and will not interact with the ORCID service'); - return; - } - - $uri = OrcidManager::getApiPath($this->context) . ORCID_API_VERSION_URL . $this->authorOrcid . '/' . ORCID_WORK_URL; - $method = 'POST'; - - if ($putCode = $this->author->getData('orcidWorkPutCode')) { - // Submission has already been sent to ORCID. Use PUT to update meta data - $uri .= '/' . $putCode; - $method = 'PUT'; - $orcidWork['put-code'] = $putCode; - } else { - // Remove put-code from body because the work has not yet been sent - unset($this->orcidWork['put-code']); - } - - $headers = [ - 'Content-type: application/vnd.orcid+json', - 'Accept' => 'application/json', - 'Authorization' => 'Bearer ' . $this->author->getData('orcidAccessToken') - ]; - - OrcidManager::logInfo("{$method} {$uri}"); - OrcidManager::logInfo('Header: ' . var_export($headers, true)); - - $httpClient = Application::get()->getHttpClient(); - try { - $response = $httpClient->request( - $method, - $uri, - [ - 'headers' => $headers, - 'json' => $this->orcidWork, - ] - ); - } catch (ClientException $exception) { - $reason = $exception->getResponse()->getBody(); - OrcidManager::logError("Publication fail: {$reason}"); - - $this->fail($exception); - } - $httpStatus = $response->getStatusCode(); - OrcidManager::logInfo("Response status: {$httpStatus}"); - $responseHeaders = $response->getHeaders(); - - switch ($httpStatus) { - case 200: - // Work updated - OrcidManager::logInfo("Work updated in profile, putCode: {$putCode}"); - // TODO: See what to do with request success variable. Won't be handled by job in anyway by default - $requestSuccess = true; - break; - case 201: - $location = $responseHeaders['Location'][0]; - // Extract the ORCID work put code for updates/deletion. - $putCode = intval(basename(parse_url($location, PHP_URL_PATH))); - OrcidManager::logInfo("Work added to profile, putCode: {$putCode}"); - $this->author->setData('orcidWorkPutCode', $putCode); - Repo::author()->dao->update($this->author); - $requestSuccess = true; - break; - case 401: - // invalid access token, token was revoked - $error = json_decode($response->getBody(), true); - if ($error['error'] === 'invalid_token') { - OrcidManager::logError($error['error_description'] . ', deleting orcidAccessToken from author'); - OrcidManager::removeOrcidAccessToken($this->author); - } - $requestSuccess = false; - break; - case 403: - OrcidManager::logError('Work update forbidden: ' . $response->getBody()); - $requestSuccess = false; - break; - case 404: - // a work has been deleted from a ORCID record. putCode is no longer valid. - if ($method === 'PUT') { - OrcidManager::logError('Work deleted from ORCID record, deleting putCode form author'); - $this->author->setData('orcidWorkPutCode', null); - Repo::author()->dao->update($this->author); - $requestSuccess = false; - } else { - OrcidManager::logError("Unexpected status {$httpStatus} response, body: " . $response->getBody()); - $requestSuccess = false; - } - break; - case 409: - OrcidManager::logError('Work already added to profile, response body: ' . $response->getBody()); - $requestSuccess = false; - break; - default: - OrcidManager::logError("Unexpected status {$httpStatus} response, body: " . $response->getBody()); - $requestSuccess = false; - } - } -}