diff --git a/api/v1/citations/PKPCitationController.php b/api/v1/citations/PKPCitationController.php index 75a23122467..6bbb04cf9d3 100644 --- a/api/v1/citations/PKPCitationController.php +++ b/api/v1/citations/PKPCitationController.php @@ -22,6 +22,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Route; +use PKP\citation\pid\Arxiv; +use PKP\citation\pid\Doi; +use PKP\citation\pid\Handle; use PKP\core\PKPBaseController; use PKP\core\PKPRequest; use PKP\plugins\Hook; @@ -173,6 +176,19 @@ public function edit(Request $illuminateRequest): JsonResponse $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CITATION, $illuminateRequest->input()); + $arxiv = Arxiv::extractFromString($params['arxiv']); + if(!empty($arxiv)){ + $params['arxiv'] = $arxiv; + } + $doi = Doi::extractFromString($params['doi']); + if(!empty($doi)){ + $params['doi'] = $doi; + } + $handle = Handle::extractFromString($params['handle']); + if(!empty($handle)){ + $params['handle'] = $handle; + } + $readOnlyErrors = $this->getWriteDisabledErrors(PKPSchemaService::SCHEMA_CITATION, $params); if (!empty($readOnlyErrors)) { return response()->json($readOnlyErrors, Response::HTTP_BAD_REQUEST); diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index 8713ddf5d70..e3d8ba986f9 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -121,8 +121,8 @@ class PKPSubmissionController extends PKPBaseController 'changeVersion', 'getNextAvailableVersion', 'importAdditionalCitations', - 'editCitationsMetadataLookup', 'deleteCitationsByPublicationId', + 'reprocessCitationsByPublicationId' ]; /** @var array Handlers that must be authorized to write to a publication */ @@ -371,10 +371,6 @@ public function getGroupRoutes(): void Role::ROLE_ID_AUTHOR, ]), ])->group(function () { - Route::put('{submissionId}/publications/{publicationId}/citations/metadataLookup', $this->editCitationsMetadataLookup(...)) - ->name('submission.citations.import') - ->whereNumber(['submissionId', 'publicationId']); - Route::post('{submissionId}/publications/{publicationId}/citations/importAdditionalCitations', $this->importAdditionalCitations(...)) ->name('submission.citations.import') ->whereNumber(['submissionId', 'publicationId']); @@ -382,6 +378,10 @@ public function getGroupRoutes(): void Route::delete('{submissionId}/publications/{publicationId}/citations/deleteCitationsByPublicationId', $this->deleteCitationsByPublicationId(...)) ->name('submission.citations.delete') ->whereNumber(['submissionId', 'publicationId']); + + Route::post('{submissionId}/publications/{publicationId}/citations/reprocessCitationsByPublicationId', $this->reprocessCitationsByPublicationId(...)) + ->name('submission.citations.reprocess') + ->whereNumber(['submissionId', 'publicationId']); }); } @@ -2335,9 +2335,9 @@ protected function validateVersionIsMinor(Request $illuminateRequest): ?bool } /** - * Update citationsMetadataLookup of a publication, reprocess citations if enabled. + * Import / add citations from a raw citation string of a publication. */ - protected function editCitationsMetadataLookup(Request $illuminateRequest): JsonResponse + protected function importAdditionalCitations(Request $illuminateRequest): JsonResponse { $publication = Repo::publication()->get((int)$illuminateRequest->route('publicationId')); @@ -2365,39 +2365,17 @@ protected function editCitationsMetadataLookup(Request $illuminateRequest): Json ], Response::HTTP_FORBIDDEN); } - $citationsMetadataLookup = (bool)$illuminateRequest->input('citationsMetadataLookup'); - - $processCitation = false; - if ($citationsMetadataLookup && !$publication->getData('citationsMetadataLookup')) { - $processCitation = true; - } - - /** @var Citation $citation */ - foreach ($publication->getData('citations') as &$citation) { - $citation->setIsProcessed(!$processCitation); - if ($processCitation) { - Repo::citation()->reprocessCitation($citation); - } - } - unset($citation); - - $publication->setData('citationsMetadataLookup', $citationsMetadataLookup); - Repo::publication()->edit($publication, []); + $rawCitations = (string)$illuminateRequest->input('rawCitations'); - $publication = Repo::publication()->get($publication->getId()); + $result = Repo::citation()->importAdditionalCitations($publication->getId(), $rawCitations); - return response()->json( - [ - 'citationsMetadataLookup' => $publication->getData('citationsMetadataLookup') - ], - Response::HTTP_OK - ); + return response()->json($result, Response::HTTP_OK); } /** - * Import / add citations from a raw citation string of a publication. + * Delete a publication's citations. */ - protected function importAdditionalCitations(Request $illuminateRequest): JsonResponse + protected function deleteCitationsByPublicationId(Request $illuminateRequest): JsonResponse { $publication = Repo::publication()->get((int)$illuminateRequest->route('publicationId')); @@ -2425,17 +2403,24 @@ protected function importAdditionalCitations(Request $illuminateRequest): JsonRe ], Response::HTTP_FORBIDDEN); } - $rawCitations = (string)$illuminateRequest->input('rawCitations'); + $existingCitations = []; + /** @var Citation $citation */ + foreach ($publication->getData('citations') as $citation) { + $existingCitations[] = Repo::citation()->getSchemaMap()->map($citation); + } - $result = Repo::citation()->importAdditionalCitations($publication->getId(), $rawCitations); + Repo::citation()->deleteByPublicationId($publication->getId()); - return response()->json($result, Response::HTTP_OK); + return response()->json([ + 'itemsMax' => count($existingCitations), + 'items' => $existingCitations + ], Response::HTTP_OK); } /** - * Delete a publication's citations. + * Reprocess a publication's citations. */ - protected function deleteCitationsByPublicationId(Request $illuminateRequest): JsonResponse + protected function reprocessCitationsByPublicationId(Request $illuminateRequest): JsonResponse { $publication = Repo::publication()->get((int)$illuminateRequest->route('publicationId')); @@ -2463,17 +2448,19 @@ protected function deleteCitationsByPublicationId(Request $illuminateRequest): J ], Response::HTTP_FORBIDDEN); } - $existingCitations = []; - /** @var Citation $citation */ - foreach ($publication->getData('citations') as $citation) { - $existingCitations[] = Repo::citation()->getSchemaMap()->map($citation); + $citations = $publication->getData('citations'); + $citationsMapped = []; + foreach ($citations as &$citation) { + $citation->setIsProcessed(false); + Repo::citation()->edit($citation, []); + Repo::citation()->reprocessCitation($citation); + $citationsMapped[] = Repo::citation()->getSchemaMap()->map($citation); } - - Repo::citation()->deleteByPublicationId($publication->getId()); + unset($citation); return response()->json([ - 'itemsMax' => count($existingCitations), - 'items' => $existingCitations + 'itemsMax' => count($citationsMapped), + 'items' => $citationsMapped ], Response::HTTP_OK); } } diff --git a/classes/citation/Repository.php b/classes/citation/Repository.php index 77cea405b00..563c4c7a3b0 100644 --- a/classes/citation/Repository.php +++ b/classes/citation/Repository.php @@ -221,7 +221,6 @@ public function importCitations(int $publicationId, ?string $rawCitationList): v $importedCitations = []; $this->deleteByPublicationId($publicationId); if (is_array($citationStrings) && !empty($citationStrings)) { - $publication = Repo::publication()->get($publicationId); foreach ($citationStrings as $seq => $rawCitationString) { if (!empty($rawCitationString)) { $citation = new Citation(); @@ -231,8 +230,7 @@ public function importCitations(int $publicationId, ?string $rawCitationList): v $citation->setIsProcessed(false); $newCitationId = $this->dao->insert($citation); $citation->setId($newCitationId); - if ($publication->getData('citationsMetadataLookup') || - (is_null($publication->getData('citationsMetadataLookup')) && $this->request->getContext()->getData('citationsMetadataLookup'))) { + if ($this->request->getContext()->getData('citationsMetadataLookup')) { $this->reprocessCitation($citation); } $importedCitations[] = $citation; @@ -256,7 +254,6 @@ public function importAdditionalCitations(int $publicationId, ?string $rawCitati $rejectedCitations = []; if (is_array($citationStrings) && !empty($citationStrings)) { - $publication = Repo::publication()->get($publicationId); foreach ($citationStrings as $rawCitationString) { if (!empty($rawCitationString)) { if (!$this->existsRawCitation($publicationId, $rawCitationString)) { @@ -268,8 +265,7 @@ public function importAdditionalCitations(int $publicationId, ?string $rawCitati $citation->setIsProcessed(false); $newCitationId = $this->dao->insert($citation); $citation->setId($newCitationId); - if ($publication->getData('citationsMetadataLookup') || - (is_null($publication->getData('citationsMetadataLookup')) && $this->request->getContext()->getData('citationsMetadataLookup'))) { + if ($this->request->getContext()->getData('citationsMetadataLookup')) { $this->reprocessCitation($citation); } } else { diff --git a/classes/citation/enum/CitationSourceType.php b/classes/citation/enum/CitationSourceType.php new file mode 100644 index 00000000000..ce8a338e320 --- /dev/null +++ b/classes/citation/enum/CitationSourceType.php @@ -0,0 +1,28 @@ +getAuthor($authorship); } break; + case 'type': + $newValue = !empty($response[$mappedKey]) ? $response[$mappedKey] : $response['type']; + break; default: if (is_array($mappedKey)) { $newValue = ExternalServicesHelper::getValueFromArrayPath($response, $mappedKey); diff --git a/classes/citation/externalServices/openAlex/Mapping.php b/classes/citation/externalServices/openAlex/Mapping.php index c234f344a32..41b28c9db01 100644 --- a/classes/citation/externalServices/openAlex/Mapping.php +++ b/classes/citation/externalServices/openAlex/Mapping.php @@ -34,7 +34,7 @@ public static function getWork(): array return [ 'title' => 'title', 'date' => 'publication_date', - 'type' => 'type_crossref', + 'type' => 'type_crossref', // use 'type' if empty 'volume' => ['biblio', 'volume'], 'issue' => ['biblio', 'issue'], 'firstPage' => ['biblio', 'first_page'], diff --git a/classes/citation/pid/Arxiv.php b/classes/citation/pid/Arxiv.php index 75200318a9a..239ef3a6cfa 100644 --- a/classes/citation/pid/Arxiv.php +++ b/classes/citation/pid/Arxiv.php @@ -19,33 +19,20 @@ class Arxiv extends BasePid { /** @copydoc AbstractPid::regex */ - public const regex = '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s'; - - /** @copydoc AbstractPid::prefix */ - public const prefix = 'https://arxiv.org/abs'; - - /** @copydoc AbstractPid::prefixInCorrect */ - public const prefixInCorrect = [ - 'arxiv:' + public const regexes = [ + '/arxiv:\s*(?:\d+.\d+|[a-z-]+.\d+)/i', // arxiv:2025.12345v2 + '/https?:\/\/arxiv.org\/(?:abs|pdf)\/(?:\d+.\d+|[a-z-]+.\d+)/i' // https://arxiv.org/abs/2025.12345 ]; - /** @copydoc AbstractPid::extractFromString() */ - public static function extractFromString(?string $string): string - { - $string = parent::extractFromString($string); - - $class = get_called_class(); - - // check if prefix found in extracted string - $prefixes = $class::prefixInCorrect; - $prefixes[] = $class::prefix; + /** @copydoc AbstractPid::prefix */ + public const prefix = 'arxiv:'; - foreach ($prefixes as $prefix) { - if (str_contains($string, $prefix)) { - return $string; - } - } + /** @copydoc AbstractPid::urlPrefix */ + public const urlPrefix = 'https://arxiv.org/abs/'; - return ''; - } + /** @copydoc AbstractPid::alternatePrefixes */ + public const alternatePrefixes = [ + 'arxiv', + 'https://arxiv.org/pdf/' + ]; } diff --git a/classes/citation/pid/BasePid.php b/classes/citation/pid/BasePid.php index 98765c98e9a..f3e1ac363ab 100644 --- a/classes/citation/pid/BasePid.php +++ b/classes/citation/pid/BasePid.php @@ -18,24 +18,27 @@ abstract class BasePid { - /** @var string Regex to extract PID */ - public const regex = ''; + /** @var string Regexes to extract PIDs */ + public const regexes = []; - /** @var string Correct prefix, e.g. https://doi.org */ + /** @var string Default prefix, e.g. doi: arxiv: handle: */ public const prefix = ''; - /** @var array|string[] Incorrect prefixes; omit http:// https:// */ - public const prefixInCorrect = []; + /** @var string Url prefix, e.g. https://doi.org/ https://arxiv.org/abs/ https://hdl.handle.net/ */ + public const urlPrefix = ''; + + /** @var array|string[] Alternate prefixes */ + public const alternatePrefixes = []; /** @var string Default characters which are trimmed */ public const defaultTrimCharacters = ' ./'; /** - * Add prefix + * Add prefix. * * @param string|null $string e.g. 10.123/tib123 * - * @return string e.g. https://doi.org10.123/tib123 + * @return string e.g. https://doi.org/10.123/tib123 */ public static function addPrefix(?string $string): string { @@ -46,24 +49,17 @@ public static function addPrefix(?string $string): string /* @var BasePid $class */ $class = get_called_class(); - // no prefix defined, return original string - if (empty($class::prefix)) { - return $string; - } - - $string = trim($string, $class::defaultTrimCharacters); - - return $class::prefix . '/' . $string; + return $class::prefix . $string; } /** - * Remove prefix + * Add urlPrefix. * - * @param string|null $string e.g. https://doi.org10.123/tib123 + * @param string|null $string e.g. 10.123/tib123 * - * @return string e.g. 10.123/tib123 + * @return string e.g. https://doi.org/10.123/tib123 */ - public static function removePrefix(?string $string): string + public static function addUrlPrefix(?string $string): string { if (empty($string)) { return ''; @@ -72,24 +68,17 @@ public static function removePrefix(?string $string): string /* @var BasePid $class */ $class = get_called_class(); - // no prefix defined, return original string - if (empty($class::prefix)) { - return $string; - } - - $string = str_ireplace($class::prefix, '', $string); - - return trim($string, $class::defaultTrimCharacters); + return $class::urlPrefix . $string; } /** - * Normalize PID by removing any incorrect prefixes. + * Remove prefixes. * - * @param string|null $string e.g. doi:10.123/tib123 + * @param string|null $string e.g. doi:10.123/tib123 https://doi.org/10.123/tib123 * - * @return string e.g. https://doi.org/10.123/tib123 + * @return string e.g. 10.123/tib123 */ - public static function normalize(?string $string): string + public static function removePrefix(?string $string): string { if (empty($string)) { return ''; @@ -98,75 +87,80 @@ public static function normalize(?string $string): string /* @var BasePid $class */ $class = get_called_class(); - // no prefix defined, return original string - if (empty($class::prefix)) { - return $string; + return trim( + str_ireplace($class::getPrefixes(), '', $string), + $class::defaultTrimCharacters + ); + } + + /** + * Remove all instances of prefix . pid from string. + */ + public static function removePrefixesWithPid(?string $pid, ?string $string): string + { + if (empty($pid) || empty($string)) { + return $string ?: ''; } - $prefixInCorrect = $class::prefixInCorrect; - - // prefix without https:// - $prefixAlt = str_ireplace('https://', '', $class::prefix); - - // make secure - $string = str_ireplace('http://', 'https://', $string); - - // process longer first, e.g. dx.doi.org before doi.org - usort($prefixInCorrect, function ($a, $b) { - return strlen($b) - strlen($a); - }); - - // common mistakes, e.g. doi.org:10.123/tib123 - $fixes = [ - $prefixInCorrect, - "{$prefixAlt}: ", - "{$prefixAlt}:", - "{$prefixAlt} ", - "www.{$prefixAlt}" - ]; - $string = str_ireplace($fixes, $prefixAlt, $string); - - // add https:// - $string = str_replace($prefixAlt, "https://{$prefixAlt}/", $string); - - // clean doubles - $doubles = [ - "https://{$prefixAlt}//", - "https://https://{$prefixAlt}/", - "https://https://{$prefixAlt}//" - ]; - $string = str_ireplace($doubles, "https://{$prefixAlt}/", $string); - - return trim($string, $class::defaultTrimCharacters); + /* @var BasePid $class */ + $class = get_called_class(); + + return trim( + str_replace( + array_map(fn($prefix) => $prefix . $pid, $class::getPrefixes()), + '', + $string + ) + ); } /** - * Extract from string with regex + * Extract from string with regex. * * @return string e.g. 10.123/tib123 */ public static function extractFromString(?string $string): string { - if (empty($string)) { - return ''; - } - /* @var BasePid $class */ $class = get_called_class(); - // no regex defined, return empty - if (empty($class::regex)) { - return ''; + if (empty($class::regexes)) { + return $string ?: ''; } - $matches = []; - - preg_match($class::regex, $string, $matches); + $match = ''; + foreach ($class::regexes as $regex) { + if (preg_match($regex, $string, $matches)) { + $match = $matches[0]; + break; + } + } - if (empty($matches[0])) { + if (empty($match)) { return ''; } - return trim($matches[0], $class::defaultTrimCharacters); + return trim($class::removePrefix($match), $class::defaultTrimCharacters); + } + + /** + * Get a list of possible prefixes. + */ + public static function getPrefixes(): array + { + /* @var BasePid $class */ + $class = get_called_class(); + + $prefixes = array_merge( + [$class::prefix, $class::prefix . ' '], + [$class::urlPrefix], + $class::alternatePrefixes, + array_map(fn($value) => trim($value) . ' ', $class::alternatePrefixes) + ); + $prefixes = array_filter($prefixes, fn($value) => !empty(trim($value))); + $prefixes = array_unique($prefixes); + usort($prefixes, fn($a, $b) => strlen($b) - strlen($a)); + + return $prefixes; } } diff --git a/classes/citation/pid/Doi.php b/classes/citation/pid/Doi.php index db33421866b..78eceab5c10 100644 --- a/classes/citation/pid/Doi.php +++ b/classes/citation/pid/Doi.php @@ -22,14 +22,23 @@ class Doi extends BasePid { /** @copydoc AbstractPid::regex */ - public const regex = '(10[.][0-9]{4,}[^\s"/<>]*/[^\s"<>]+)'; + public const regexes = [ + '/doi:\s*10[.][0-9]{4,}\/[^\s"<>]+/i', + '/https?:\/\/doi\.org\/10[.][0-9]{4,}\/[^\s"<>]+/i' + ]; /** @copydoc AbstractPid::prefix */ - public const prefix = 'https://doi.org'; + public const prefix = 'doi:'; + + /** @copydoc AbstractPid::urlPrefix */ + public const urlPrefix = 'https://doi.org/'; - /** @copydoc AbstractPid::prefixInCorrect */ - public const prefixInCorrect = [ - 'doi:', - 'dx.doi.org' + /** @copydoc AbstractPid::alternatePrefixes */ + public const alternatePrefixes = [ + 'doi', + 'doi.org', + 'doi.org:', + 'dx.doi.org', + 'dx.doi.org:' ]; } diff --git a/classes/citation/pid/ExtractPidsHelper.php b/classes/citation/pid/ExtractPidsHelper.php index f767954f2ca..d5612ab2752 100644 --- a/classes/citation/pid/ExtractPidsHelper.php +++ b/classes/citation/pid/ExtractPidsHelper.php @@ -22,28 +22,34 @@ class ExtractPidsHelper { public function execute(Citation $citation): Citation { - $raw = $citation->getRawCitation(); + $raw = str_ireplace('http://', 'https://', $citation->getRawCitation()); - // extract doi + // doi $doi = Doi::extractFromString($raw); if (!empty($doi)) { $citation->setData('doi', $doi); + $raw = Doi::removePrefixesWithPid($doi, $raw); } - // remove doi from raw - $raw = str_replace(Doi::addPrefix($doi), '', Doi::normalize($raw)); - - // parse url (after parsing doi) - $url = Url::extractFromString($raw); - $handle = Handle::extractFromString($raw); + // arxiv $arxiv = Arxiv::extractFromString($raw); + if (!empty($arxiv)) { + $citation->setData('arxiv', $arxiv); + $raw = Arxiv::removePrefixesWithPid($arxiv, $raw); + } + // handle + $handle = Handle::extractFromString($raw); if (!empty($handle)) { $citation->setData('handle', $handle); - } else if (!empty($arxiv)) { - $citation->setData('arxiv', $arxiv); - } else if (!empty($url)) { + $raw = Handle::removePrefixesWithPid($handle, $raw); + } + + // url + $url = Url::extractFromString($raw); + if (!empty($url)) { $citation->setData('url', $url); + $raw = str_replace($url, '', $raw); } // urn diff --git a/classes/citation/pid/Handle.php b/classes/citation/pid/Handle.php index 7a5f4868d99..305dfeeb572 100644 --- a/classes/citation/pid/Handle.php +++ b/classes/citation/pid/Handle.php @@ -19,33 +19,21 @@ class Handle extends BasePid { /** @copydoc AbstractPid::regex */ - public const regex = '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s'; - - /** @copydoc AbstractPid::prefix */ - public const prefix = 'https://hdl.handle.net'; - - /** @copydoc AbstractPid::prefixInCorrect */ - public const prefixInCorrect = [ - 'handle:' + public const regexes = [ + '/(?:handle|hdl):\s*[0-9a-z]+(?:.[0-9a-z]+)*\/.+/i', // handle:12345/abcde hdl:12345/abcde + '/https?:\/\/hdl\.handle\.net\/[0-9a-z]+(?:.[0-9a-z]+)*\/.+/i', // https://hdl.handle.net/12345/abcde ]; - /** @copydoc AbstractPid::extractFromString() */ - public static function extractFromString(?string $string): string - { - $string = parent::extractFromString($string); - - $class = get_called_class(); - - // check if prefix found in extracted string - $prefixes = $class::prefixInCorrect; - $prefixes[] = $class::prefix; + /** @copydoc AbstractPid::prefix */ + public const prefix = 'handle:'; - foreach ($prefixes as $prefix) { - if (str_contains($string, $prefix)) { - return $string; - } - } + /** @copydoc AbstractPid::urlPrefix */ + public const urlPrefix = 'https://hdl.handle.net/'; - return ''; - } + /** @copydoc AbstractPid::alternatePrefixes */ + public const alternatePrefixes = [ + 'handle', + 'hdl', + 'hdl:' + ]; } diff --git a/classes/citation/pid/OpenAlex.php b/classes/citation/pid/OpenAlex.php deleted file mode 100644 index dac2dfc8ace..00000000000 --- a/classes/citation/pid/OpenAlex.php +++ /dev/null @@ -1,30 +0,0 @@ -]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s'; + public const regexes = [ + '#(http|https|ftp)://[\d\w\.-]+\.[\w\.]{2,6}[^\s\]\[\<\>]*/?#' + ]; } diff --git a/classes/citation/pid/Urn.php b/classes/citation/pid/Urn.php index 65aaa624468..9c614525a4d 100644 --- a/classes/citation/pid/Urn.php +++ b/classes/citation/pid/Urn.php @@ -19,5 +19,7 @@ class Urn extends BasePid { /** @copydoc AbstractPid::regex */ - public const regex = '/urn:([a-z0-9][a-z0-9-]{1,31}):((?:[-a-z0-9()+,.:=@;$_!*\'&~\/]|%[0-9a-f]{2})+)(?:(\?\+)((?:(?!\?=)(?:[-a-z0-9()+,.:=@;$_!*\'&~\/\?]|%[0-9a-f]{2}))*))?(?:(\?=)((?:(?!#).)*))?(?:(#)((?:[-a-z0-9()+,.:=@;$_!*\'&~\/\?]|%[0-9a-f]{2})*))?$/i'; + public const regexes = [ + '/urn:([a-z0-9][a-z0-9-]{1,31}):((?:[-a-z0-9()+,.:=@;$_!*\'&~\/]|%[0-9a-f]{2})+)(?:(\?\+)((?:(?!\?=)(?:[-a-z0-9()+,.:=@;$_!*\'&~\/\?]|%[0-9a-f]{2}))*))?(?:(\?=)((?:(?!#).)*))?(?:(#)((?:[-a-z0-9()+,.:=@;$_!*\'&~\/\?]|%[0-9a-f]{2})*))?$/i' + ]; } diff --git a/classes/components/forms/citation/CitationStructuredEditForm.php b/classes/components/forms/citation/CitationStructuredEditForm.php index 092728c8334..5ec5ab82b78 100644 --- a/classes/components/forms/citation/CitationStructuredEditForm.php +++ b/classes/components/forms/citation/CitationStructuredEditForm.php @@ -16,7 +16,10 @@ namespace PKP\components\forms\citation; +use PKP\citation\enum\CitationSourceType; +use PKP\citation\enum\CitationType; use PKP\components\forms\FieldAuthors; +use PKP\components\forms\FieldSelect; use PKP\components\forms\FieldText; use PKP\components\forms\FieldTextarea; use PKP\components\forms\FormComponent; @@ -83,7 +86,14 @@ public function __construct(string $action) 'description' => '', 'value' => null, ])); - $this->addField(new FieldText('sourceType', [ + $this->addField(new FieldSelect('sourceType', [ + 'options' => array_map( + fn($case) => [ + 'label' => ucwords($case->value), + 'value' => $case->value + ], + CitationSourceType::cases() + ), 'label' => __('submission.citations.structured.label.sourceType'), 'description' => '', 'value' => null, @@ -94,7 +104,14 @@ public function __construct(string $action) 'value' => null, 'inputType' => 'date' ])); - $this->addField(new FieldText('type', [ + $this->addField(new FieldSelect('type', [ + 'options' => array_map( + fn($case) => [ + 'label' => ucwords(str_replace('-', ' ', $case->value)), + 'value' => $case->value + ], + CitationType::cases() + ), 'label' => __('submission.citations.structured.label.type'), 'description' => '', 'value' => null, diff --git a/locale/en/manager.po b/locale/en/manager.po index 2765241c2a3..3fa22dfc79b 100644 --- a/locale/en/manager.po +++ b/locale/en/manager.po @@ -2358,7 +2358,7 @@ msgstr "" "Require the author to provide references before accepting their submission." msgid "manager.setup.metadata.citationsMetadataLookup.enable" -msgstr "Enable references metadata lookup for new submissions" +msgstr "Enable references structuring and metadata lookup" msgid "manager.setup.metadata.dataAvailability.description" msgstr "" diff --git a/locale/en/submission.po b/locale/en/submission.po index 8d4657a5c9a..97d20eae8e5 100644 --- a/locale/en/submission.po +++ b/locale/en/submission.po @@ -486,8 +486,8 @@ msgstr "" msgid "submission.citations.structured.citationsMetadataLookup.description" msgstr "" -"This section helps you structure your references. " -"Clicking \"Enable Metadata Lookup\" will allow the system to process your references and retrieve DOIs and other metadata from external sources. " +"Structuring and Metadata Lookup is enabled for this Journal. " +"The system will process your references and retrieve DOIs and other metadata from external sources. " "This may take some time, but you can continue working on your submission and return to this page later to view the updated structured citations. " msgid "submission.citations.structured.descriptionTable" @@ -549,14 +549,23 @@ msgid "submission.citations.structured.deleteDialog.confirm" msgstr "This will remove this citation." msgid "submission.citations.structured.deleteAllLink" -msgstr "Delete All Structured References" +msgstr "Delete all references" msgid "submission.citations.structured.deleteAllDialog.title" -msgstr "Delete all Structured Citations" +msgstr "Delete all references" msgid "submission.citations.structured.deleteAllDialog.confirm" msgstr "This will remove all references currently listed. You'll need to re-enter and process your citations again if you continue." +msgid "submission.citations.structured.reprocessAllCitations" +msgstr "Reprocess all references" + +msgid "submission.citations.structured.reprocessAllCitations.title" +msgstr "Reprocess all references" + +msgid "submission.citations.structured.reprocessAllCitations.confirm" +msgstr "This will reprocess all references currently listed. You'll need to re-enter your manual changes again if you continue." + msgid "submission.citations.structured.collapseAll" msgstr "Collapse All" @@ -600,7 +609,7 @@ msgid "submission.citations.structured.label.rawCitation" msgstr "Edit Raw Citation" msgid "submission.citations.structured.label.doi" -msgstr "DOI" +msgstr "DOI, e.g. 10.1000/182, doi:10.1000/182, https://doi.org/10.1000/182" msgid "submission.citations.structured.label.title" msgstr "Title" @@ -648,10 +657,10 @@ msgid "submission.citations.structured.label.urn" msgstr "URN" msgid "submission.citations.structured.label.arxiv" -msgstr "Arxiv" +msgstr "Arxiv, e.g. 1234.123456v2, arxiv:1234.123456v2, https://arxiv.org/abs/1234.123456v2" msgid "submission.citations.structured.label.handle" -msgstr "Handle" +msgstr "Handle, e.g. 20.1000/100, handle:20.1000/100, https://hdl.handle.net/20.1000/100" msgid "submission.citations.structured.label.openAlex" msgstr "OpenAlex" diff --git a/schemas/citation.json b/schemas/citation.json index 4b2d7ae1340..f90bdd2e626 100644 --- a/schemas/citation.json +++ b/schemas/citation.json @@ -15,12 +15,12 @@ }, "arxiv": { "type": "string", - "description": "Arxiv id.", + "description": "Arxiv id, e.g. 1902.02534.", "multilingual": false, "apiSummary": true, "validation": [ "nullable", - "url" + "regex:/^(?:\\d+.\\d+|[a-z-]+.\\d+)/i" ] }, "authors": { @@ -45,6 +45,9 @@ }, "openAlex": { "type": "string" + }, + "wikidata": { + "type": "string" } } } @@ -61,12 +64,12 @@ }, "doi": { "type": "string", - "description": "The DOI itself, such as `10.1234/5a6b-7c8d`.", + "description": "The DOI itself, e.g. 10.1234/5a6b-7c8d.", "multilingual": false, "apiSummary": true, "validation": [ "nullable", - "regex:/^\\d+(.\\d+)+\\//" + "regex:/^10[.][0-9]{4,}\\/[^\\s\"<>]+/i" ] }, "firstPage": { @@ -80,12 +83,12 @@ }, "handle": { "type": "string", - "description": "Handle id.", + "description": "Handle id, e.g. 11366/1249.", "multilingual": false, "apiSummary": true, "validation": [ "nullable", - "url" + "regex:/^[0-9a-z]+(?:.[0-9a-z]+)*\\/.+/i" ] }, "id": { @@ -197,7 +200,7 @@ "apiSummary": true, "validation": [ "nullable", - "in:journal,repository,conference,ebookplatform,bookseries,metadata,other" + "in:book series,conference,ebook platform,journal,metadata,other,repository" ] }, "title": { @@ -216,7 +219,7 @@ "apiSummary": true, "validation": [ "nullable", - "in:book,book-chapter,book-part,book-section,book-series,book-set,book-track,component,database,dataset,dissertation,edited-book,grant,journal,journal-article,journal-issue,journal-volume,monograph,other,peer-review,posted-content,proceedings,proceedings-article,proceedings-series,reference-book,reference-entry,report,report-component,report-series,standard" + "in:book,book-chapter,book-part,book-section,book-series,book-set,book-track,component,database,dataset,dissertation,edited-book,editorial,erratum,grant,journal,journal-article,journal-issue,journal-volume,letter,libguides,monograph,other,paratext,peer-review,posted-content,preprint,proceedings,proceedings-article,proceedings-series,reference-book,reference-entry,report,report-component,report-series,retraction,review,standard,supplementary-materials" ] }, "url": { diff --git a/schemas/publication.json b/schemas/publication.json index b35d404f7b4..cd09f1e1bec 100644 --- a/schemas/publication.json +++ b/schemas/publication.json @@ -78,13 +78,6 @@ "nullable" ] }, - "citationsMetadataLookup": { - "type": "boolean", - "description": "Whether or not to use the metadata lookup for citations.", - "validation": [ - "nullable" - ] - }, "copyrightHolder": { "type": "string", "description": "The copyright statement for this publication.",