diff --git a/CrossrefExportPlugin.php b/CrossrefExportPlugin.php
index 4c34838..6baaebb 100644
--- a/CrossrefExportPlugin.php
+++ b/CrossrefExportPlugin.php
@@ -30,6 +30,7 @@
use PKP\file\FileManager;
use PKP\file\TemporaryFileManager;
use PKP\plugins\Hook;
+use PKP\submission\reviewAssignment\ReviewAssignment;
class CrossrefExportPlugin extends DOIPubIdExportPlugin
{
@@ -83,6 +84,14 @@ public function getSubmissionFilter()
return 'article=>crossref-xml';
}
+ /**
+ * @copydoc PubObjectsExportPlugin::getPeerReviewFilter()
+ */
+ public function getPeerReviewFilter(): string
+ {
+ return 'peerReview=>crossref-xml';
+
+ }
/**
* @copydoc PubObjectsExportPlugin::getIssueFilter()
*/
@@ -283,7 +292,7 @@ public function exportAsDownload(Journal $context, array $objects, string $filte
}
/**
- * @param Submission $objects
+ * @param mixed $objects
* @param Journal $context
* @param string $filename Export XML filename
*
@@ -407,12 +416,14 @@ public function depositXML($objects, $context, $filename)
/**
* Update the local DOI deposit status and related metadata for the given object.
*/
- public function updateDepositStatus(Journal $context, Issue|Submission $object, int $status, ?string $batchId = null, ?string $failedMsg = null, ?string $successMsg = null)
+ public function updateDepositStatus(Journal $context, Issue|Submission|ReviewAssignment $object, int $status, ?string $batchId = null, ?string $failedMsg = null, ?string $successMsg = null)
{
if ($object instanceof Submission) {
$doiIds = Repo::doi()->getDoisForSubmission($object->getId());
- } else {
+ } else if ($object instanceof Issue) {
$doiIds = Repo::doi()->getDoisForIssue($object->getId(), true);
+ } else {
+ $doiIds = Repo::doi()->getDoisForReviewAssignment($object->getId(), true);
}
foreach ($doiIds as $doiId) {
@@ -437,11 +448,15 @@ public function updateDepositStatus(Journal $context, Issue|Submission $object,
/**
* Get part of the file name based on the object that is being exported
*/
- private function _getObjectFileNamePart(Submission|Issue $object): string
+ private function _getObjectFileNamePart(Submission|Issue|ReviewAssignment $object): string
{
if ($object instanceof Submission) {
return 'articles-' . $object->getId();
}
+
+ if ($object instanceof ReviewAssignment) {
+ return 'peerReviews-' . $object->getId();
+ }
return 'issues-' . $object->getId();
}
}
diff --git a/CrossrefPlugin.php b/CrossrefPlugin.php
index 30f3fb0..c5ab3e6 100644
--- a/CrossrefPlugin.php
+++ b/CrossrefPlugin.php
@@ -346,6 +346,19 @@ public function exportSubmissions(array $submissions, Context $context): array
return ['temporaryFileId' => $temporaryFileId, 'xmlErrors' => $xmlErrors];
}
+
+ /*
+ * @copyDoc IDoiRegistrationAgency::exportPeerReviews()
+ */
+ public function exportPeerReviews(array $reviewAssignments, Context $context): array
+ {
+ $filterName = $this->_exportPlugin->getPeerReviewFilter();
+ $xmlErrors = [];
+
+ $temporaryFileId = $this->_exportPlugin->exportAsDownload($context, $reviewAssignments, $filterName, 'peerReviews', null, $xmlErrors);
+
+ return ['temporaryFileId' => $temporaryFileId, 'xmlErrors' => $xmlErrors];
+ }
/**
* @param Submission[] $submissions
*/
@@ -361,6 +374,20 @@ public function depositSubmissions(array $submissions, Context $context): array
];
}
+ /*
+ * @copyDoc IDoiRegistrationAgency::depositPeerReviews()
+ */
+ public function depositPeerReviews(array $peerReviews, Context $context): array
+ {
+ $filterName = $this->_exportPlugin->getPeerReviewFilter();
+ $responseMessage = '';
+ $status = $this->_exportPlugin->exportAndDeposit($context, $peerReviews, $filterName, $responseMessage);
+
+ return [
+ 'hasErrors' => !$status,
+ 'responseMessage' => $responseMessage
+ ];
+ }
/**
* @param Issue[] $issues
*/
@@ -436,7 +463,7 @@ public function getSettingsObject(): RegistrationAgencySettings
*/
public function getAllowedDoiTypes(): array
{
- return [Repo::doi()::TYPE_PUBLICATION, Repo::doi()::TYPE_ISSUE];
+ return [Repo::doi()::TYPE_PUBLICATION, Repo::doi()::TYPE_ISSUE, Repo::doi()::TYPE_PEER_REVIEW];
}
/**
diff --git a/filter/ArticleCrossrefXmlFilter.php b/filter/ArticleCrossrefXmlFilter.php
index 7ecb3d9..aa5fb03 100644
--- a/filter/ArticleCrossrefXmlFilter.php
+++ b/filter/ArticleCrossrefXmlFilter.php
@@ -672,7 +672,18 @@ public function createComponentListNode(DOMDocument $doc, Publication $publicati
$componentNode->appendChild($titlesNode);
}
// DOI data node
- $resourceURL = $dispatcher->url($request, PKPApplication::ROUTE_PAGE, $context->getPath(), 'article', 'download', [$publication->getData('urlPath') ?? $submission->getId(), $componentGalley->getBestGalleyId()], null, null, true, '');
+ $resourceURL = $dispatcher->url(
+ $request,
+ PKPApplication::ROUTE_PAGE,
+ $context->getPath(),
+ 'article',
+ 'download',
+ [$publication->getData('urlPath') ?? $submission->getId(), $componentGalley->getBestGalleyId()],
+ null,
+ null,
+ true,
+ ''
+ );
$componentNode->appendChild($this->createDOIDataNode($doc, $componentGalley->getStoredPubId('doi'), $resourceURL));
$componentListNode->appendChild($componentNode);
}
diff --git a/filter/IssueCrossrefXmlFilter.php b/filter/IssueCrossrefXmlFilter.php
index 3df04e5..8d572d4 100644
--- a/filter/IssueCrossrefXmlFilter.php
+++ b/filter/IssueCrossrefXmlFilter.php
@@ -18,6 +18,7 @@
use APP\core\Request;
use APP\issue\Issue;
use APP\plugins\generic\crossref\CrossrefExportDeployment;
+use APP\plugins\generic\crossref\filter\trait\CrossrefFilterBuilder;
use DOMDocument;
use DOMElement;
use PKP\core\Dispatcher;
@@ -25,6 +26,8 @@
class IssueCrossrefXmlFilter extends \PKP\plugins\importexport\native\filter\NativeExportFilter
{
+ use CrossrefFilterBuilder;
+
/**
* Constructor
*
@@ -76,52 +79,6 @@ public function &process(&$pubObjects)
//
// Issue conversion functions
//
- /**
- * Create and return the root node 'doi_batch'.
- */
- public function createRootNode(DOMDocument $doc): DOMElement
- {
- /** @var CrossrefExportDeployment $deployment */
- $deployment = $this->getDeployment();
- $rootNode = $doc->createElementNS($deployment->getNamespace(), $deployment->getRootElementName());
- $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', $deployment->getXmlSchemaInstance());
- $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:jats', $deployment->getJATSNamespace());
- $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ai', $deployment->getAINamespace());
- $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:rel', $deployment->getRelNamespace());
- $rootNode->setAttribute('version', $deployment->getXmlSchemaVersion());
- $rootNode->setAttribute('xsi:schemaLocation', $deployment->getNamespace() . ' ' . $deployment->getSchemaFilename());
- return $rootNode;
- }
-
- /**
- * Create and return the head node 'head'.
- */
- public function createHeadNode(DOMDocument $doc): DOMElement
- {
- /** @var CrossrefExportDeployment $deployment */
- $deployment = $this->getDeployment();
- $context = $deployment->getContext();
- $plugin = $deployment->getPlugin();
- $headNode = $doc->createElementNS($deployment->getNamespace(), 'head');
- $headNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'doi_batch_id', htmlspecialchars($context->getData('acronym', $context->getPrimaryLocale()) . '_' . time(), ENT_COMPAT, 'UTF-8')));
- $headNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'timestamp', date('YmdHisv')));
- $depositorNode = $doc->createElementNS($deployment->getNamespace(), 'depositor');
- $depositorName = $plugin->getSetting($context->getId(), 'depositorName');
- if (empty($depositorName)) {
- $depositorName = $context->getData('supportName');
- }
- $depositorEmail = $plugin->getSetting($context->getId(), 'depositorEmail');
- if (empty($depositorEmail)) {
- $depositorEmail = $context->getData('supportEmail');
- }
- $depositorNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'depositor_name', htmlspecialchars($depositorName, ENT_COMPAT, 'UTF-8')));
- $depositorNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'email_address', htmlspecialchars($depositorEmail, ENT_COMPAT, 'UTF-8')));
- $headNode->appendChild($depositorNode);
- $publisherInstitution = $context->getData('publisherInstitution');
- $headNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'registrant', htmlspecialchars($publisherInstitution, ENT_COMPAT, 'UTF-8')));
- return $headNode;
- }
-
/**
* Create and return the journal node 'journal'.
*/
@@ -217,28 +174,4 @@ public function createDateNode(DOMDocument $doc, string $objectPublicationDate,
$publicationDateNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'year', date('Y', $publicationDate)));
return $publicationDateNode;
}
-
- /**
- * Create and return the DOI data node 'doi_data'.
- */
- public function createDOIDataNode(DOMDocument $doc, string $doi, string $url): DOMElement
- {
- $deployment = $this->getDeployment();
- $doiDataNode = $doc->createElementNS($deployment->getNamespace(), 'doi_data');
- $doiDataNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'doi', htmlspecialchars($doi, ENT_COMPAT, 'UTF-8')));
- $doiDataNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'resource', $url));
- return $doiDataNode;
- }
-
- /**
- * Helper to ensure dispatcher is available even when called from CLI tools
- */
- protected function _getDispatcher(Request $request): Dispatcher
- {
- $dispatcher = $request->getDispatcher();
- if ($dispatcher === null) {
- $dispatcher = Application::get()->getDispatcher();
- }
- return $dispatcher;
- }
}
diff --git a/filter/PeerReviewCrossrefXmlFilter.php b/filter/PeerReviewCrossrefXmlFilter.php
new file mode 100644
index 0000000..bc4f078
--- /dev/null
+++ b/filter/PeerReviewCrossrefXmlFilter.php
@@ -0,0 +1,453 @@
+setDisplayName('Crossref XML peer review export');
+ parent::__construct($filterGroup);
+ }
+
+ /**
+ * @param ReviewAssignment[] $pubObjects Array of Review Assignments
+ *
+ * @return \DOMDocument
+ * @throws \Exception
+ * @see \PKP\filter\Filter::process()
+ *
+ */
+ public function &process(&$pubObjects)
+ {
+ $this->reviewAssignments = $pubObjects;
+
+ // cache necessary data before processing reviews
+ $this->loadReviewRounds();
+ $this->loadReviewers();
+ $this->loadPublications();
+
+ // Create the XML document
+ $doc = new \DOMDocument('1.0', 'utf-8');
+ $doc->preserveWhiteSpace = false;
+ $doc->formatOutput = true;
+ /** @var CrossrefExportDeployment $deployment */
+ $deployment = $this->getDeployment();
+ $context = $deployment->getContext();
+
+ // Create the root node
+ $rootNode = $this->createRootNode($doc);
+ $doc->appendChild($rootNode);
+
+ $rootNode->appendChild($this->createHeadNode($doc));
+
+ $bodyNode = $doc->createElementNS($deployment->getNamespace(), 'body');
+ $rootNode->appendChild($bodyNode);
+
+ foreach ($pubObjects as $reviewAssignment) {
+ $peerReviewNode = $doc->createElementNS($deployment->getNamespace(), 'peer_review');
+ $publication = $this->getPublication($reviewAssignment);
+ $locale = $publication->getData('locale');
+
+ /**
+ * Set Review round attribute. See https://www.crossref.org/documentation/schema-library/markup-guide-record-types/peer-reviews/#:~:text=Revision%20round%20number%2C%20first%20submission%20is%20defined%20as%20revision%20round%200
+ */
+ $peerReviewNode->setAttribute('revision-round', $reviewAssignment->getRound() - 1);
+ $peerReviewNode->setAttribute('language', \Locale::getPrimaryLanguage($locale));
+ $bodyNode->appendChild($peerReviewNode);
+
+ $peerReviewNode->appendChild($this->createContributorsNode($doc, $reviewAssignment));
+ $peerReviewNode->appendChild($this->createTitlesNode($doc, $reviewAssignment));
+ $peerReviewNode->appendChild($this->createReviewDateNode($doc, $reviewAssignment));
+
+ if ($reviewAssignment->getCompetingInterests()) {
+ $peerReviewNode->appendChild($this->createCompetingInterestNode($doc, $reviewAssignment));
+ }
+
+ $peerReviewNode->appendChild($this->createRunningNumberNode($doc, $reviewAssignment));
+ $peerReviewNode->appendChild($this->createRelationshipNode($doc, $reviewAssignment));
+
+ $request = Application::get()->getRequest();
+ $dispatcher = $this->_getDispatcher($request);
+
+ $resourceURL = $dispatcher->url(
+ $request,
+ PKPApplication::ROUTE_PAGE,
+ $context->getPath(),
+ 'article',
+ 'view',
+ [
+ $publication->getData('urlPath') ??
+ Repo::submission()->get($publication->getData('submissionId'))->getId(),
+ ],
+ [
+ 'tab' =>'peer-review-record',
+ 'reviewId' => $reviewAssignment->getId(),
+ ],
+ null,
+ true,
+ ''
+ );
+
+ $peerReviewNode->appendChild($this->createDoiDataNode($doc, $reviewAssignment->getDoi(), $resourceURL));
+ }
+
+ return $doc;
+ }
+
+ /**
+ * Create the contributor node.
+ * @param DOMDocument $doc
+ * @param ReviewAssignment $reviewAssignment
+ * @return DOMElement
+ * @throws \DOMException
+ */
+ private function createContributorsNode(DOMDocument $doc, ReviewAssignment $reviewAssignment): DOMElement
+ {
+ /** @var CrossrefExportDeployment $deployment */
+ $deployment = $this->getDeployment();
+ $contributorsNode = $doc->createElementNS($deployment->getNamespace(), 'contributors');
+
+ $reviewer = $this->getReviewer($reviewAssignment);
+ $isOpenReview = $reviewAssignment->getReviewMethod() === ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN;
+ $publication = $this->getPublication($reviewAssignment);
+
+ $locale = $publication->getData('locale');
+
+ if ($isOpenReview) {
+ $familyNames = $reviewer->getFamilyName(null);
+ $givenNames = $reviewer->getGivenName(null);
+ $personNameNode = $doc->createElementNS($deployment->getNamespace(), 'person_name');
+ $personNameNode->setAttribute('contributor_role', 'reviewer-external');
+ $personNameNode->setAttribute('sequence', 'first');
+
+ // Check if both givenName and familyName are set for the submission language.
+ if (!empty($familyNames[$locale]) && !empty($givenNames[$locale])) {
+ $personNameNode->setAttribute('language', \Locale::getPrimaryLanguage($locale));
+ $personNameNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'given_name', htmlspecialchars($givenNames[$locale], ENT_COMPAT, 'UTF-8')));
+ $personNameNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'surname', htmlspecialchars($familyNames[$locale], ENT_COMPAT, 'UTF-8')));
+ } else {
+ $personNameNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'surname', htmlspecialchars($givenNames[$locale], ENT_COMPAT, 'UTF-8')));
+ }
+
+ $affiliation = $reviewer->getAffiliation($locale);
+ if ($affiliation) {
+ $affiliationsNode = $doc->createElementNS($deployment->getNamespace(), 'affiliations');
+ $institutionNode = $doc->createElementNS($deployment->getNamespace(), 'institution');
+ $institutionNameNode = $doc->createElementNS($deployment->getNamespace(), 'institution_name', htmlspecialchars($affiliation, ENT_COMPAT, 'UTF-8'));
+ $institutionNode->appendChild($institutionNameNode);
+ $affiliationsNode->appendChild($institutionNode);
+ $personNameNode->appendChild($affiliationsNode);
+ }
+
+ if ($reviewer->getData('orcid')) {
+ $orcidNode = $doc->createElementNS($deployment->getNamespace(), 'ORCID', $reviewer->getData('orcid'));
+ $orcidAuthenticated = $reviewer->getData('orcidIsVerified') ? 'true' : 'false';
+ $orcidNode->setAttribute('authenticated', $orcidAuthenticated);
+ $personNameNode->appendChild($orcidNode);
+ }
+
+ if (!empty($familyNames[$locale]) && !empty($givenNames[$locale])) {
+ $hasAltName = false;
+ foreach ($familyNames as $otherLocal => $familyName) {
+ if ($otherLocal != $locale && isset($familyName) && !empty($familyName)) {
+ if (!$hasAltName) {
+ $altNameNode = $doc->createElementNS($deployment->getNamespace(), 'alt-name');
+ $personNameNode->appendChild($altNameNode);
+ $hasAltName = true;
+ }
+
+ $nameNode = $doc->createElementNS($deployment->getNamespace(), 'name');
+ $nameNode->setAttribute('language', \Locale::getPrimaryLanguage($otherLocal));
+
+ $nameNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'surname', htmlspecialchars($familyName, ENT_COMPAT, 'UTF-8')));
+ if (!empty($givenNames[$otherLocal])) {
+ $nameNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'given_name', htmlspecialchars($givenNames[$otherLocal], ENT_COMPAT, 'UTF-8')));
+ }
+
+ $altNameNode->appendChild($nameNode);
+ }
+ }
+ }
+
+ $contributorsNode->appendChild($personNameNode);
+ } else {
+ $anonymousReviewerNode = $doc->createElementNS($deployment->getNamespace(), 'anonymous');
+ $anonymousReviewerNode->setAttribute('contributor_role', 'reviewer-external');
+ $anonymousReviewerNode->setAttribute('sequence', 'first');
+ $contributorsNode->appendChild($anonymousReviewerNode);
+ }
+
+ return $contributorsNode;
+ }
+
+ /**
+ * Create the titles node.
+ * @param DOMDocument $doc
+ * @param ReviewAssignment $reviewAssignment
+ * @return DOMElement
+ * @throws \DOMException
+ */
+ private function createTitlesNode(DOMDocument $doc, ReviewAssignment $reviewAssignment): DOMElement
+ {
+ $deployment = $this->getDeployment();
+ $publication = $this->getPublication($reviewAssignment);
+
+ $titlesNode = $doc->createElementNS($deployment->getNamespace(), 'titles');
+ $titleText = __('plugins.importexport.crossref.reviewTitle', [
+ 'publicationTitle' => $publication->getLocalizedTitle($publication->getData('locale')),
+ ]);
+
+ $titleNode = $doc->createElementNS(
+ $deployment->getNamespace(),
+ 'title',
+ htmlspecialchars($titleText, ENT_COMPAT, 'UTF-8')
+ );
+
+ $titlesNode->appendChild($titleNode);
+
+ return $titlesNode;
+ }
+
+ /**
+ * Create the review date node.
+ * @param DOMDocument $doc
+ * @param ReviewAssignment $reviewAssignment
+ * @return DOMElement
+ * @throws \DOMException
+ */
+ private function createReviewDateNode(DOMDocument $doc, ReviewAssignment $reviewAssignment): DOMElement
+ {
+ $deployment = $this->getDeployment();
+
+ $reviewDateNode = $doc->createElementNS($deployment->getNamespace(), 'review_date');
+ $dateParsed = Carbon::parse($reviewAssignment->getDateCompleted());
+
+ $reviewDateNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'month', $dateParsed->month));
+ $reviewDateNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'day', $dateParsed->day));
+ $reviewDateNode->appendChild($doc->createElementNS($deployment->getNamespace(), 'year', $dateParsed->year));
+
+ return $reviewDateNode;
+ }
+
+ /**
+ * Create the competing interest node.
+ * @param DOMDocument $doc
+ * @param ReviewAssignment $reviewAssignment
+ * @return DOMElement
+ * @throws \DOMException
+ */
+ private function createCompetingInterestNode(DOMDocument $doc, ReviewAssignment $reviewAssignment): DOMElement
+ {
+ $deployment = $this->getDeployment();
+ return $doc->createElementNS($deployment->getNamespace(), 'competing_interest_statement', htmlspecialchars($reviewAssignment->getCompetingInterests(), ENT_COMPAT, 'UTF-8'));
+ }
+
+ /**
+ * Create the running number node.
+ * @param DOMDocument $doc
+ * @param ReviewAssignment $reviewAssignment
+ * @return DOMElement
+ * @throws \DOMException
+ */
+ private function createRunningNumberNode(DOMDocument $doc, ReviewAssignment $reviewAssignment): DOMElement
+ {
+ $deployment = $this->getDeployment();
+ return $doc->createElementNS($deployment->getNamespace(), 'running_number', $reviewAssignment->getId());
+ }
+
+ /**
+ * Create the relationship node to associate a review with a publication.
+ * @param DOMDocument $doc
+ * @param ReviewAssignment $reviewAssignment
+ * @return DOMElement
+ * @throws \DOMException
+ */
+ private function createRelationshipNode(DOMDocument $doc, ReviewAssignment $reviewAssignment): DOMElement
+ {
+ /** @var CrossrefExportDeployment $deployment */
+ $deployment = $this->getDeployment();
+
+ $relationsNs = $deployment->getRelNamespace();
+ $programNode = $doc->createElementNS($relationsNs, 'rel:program');
+ $relatedItemNode = $doc->createElementNS($relationsNs, 'rel:related_item');
+ $publication = $this->getPublication($reviewAssignment);
+
+
+ $doiVersioning =$deployment->getContext()->getData(Context::SETTING_DOI_VERSIONING);
+ // If same DOI is used for all publication versions then link the peer review to the current publication as that publication would be the one deposited to crossref
+ $publicationDoi = null;
+ if (!$doiVersioning) {
+ $submission = Repo::submission()->get($publication->getData('submissionId'));
+ $currentPublication = Repo::publication()->get($submission->getCurrentPublication()->getId());
+ $publicationDoi = $currentPublication->getDoi();
+ } else {
+ $publicationDoi = $publication->getDoi();
+ }
+
+ $InterRelationNode = $doc->createElementNS($relationsNs, 'rel:inter_work_relation', $publicationDoi);
+ $InterRelationNode->setAttribute('relationship-type', 'isReviewOf');
+ $InterRelationNode->setAttribute('identifier-type', 'doi');
+
+ $relatedItemNode->appendChild($InterRelationNode);
+ $programNode->appendChild($relatedItemNode);
+
+ return $programNode;
+ }
+
+ /**
+ * Get the publication for a review assignment.
+ */
+ private function getPublication(ReviewAssignment $reviewAssignment): Publication
+ {
+ $reviewRound = $this->getReviewRound($reviewAssignment->getReviewRoundId());
+
+ if (empty($this->publications)) {
+ $this->loadPublications();
+ }
+
+ /** @var Publication $publication */
+ $publication = $this->publications->get($reviewRound->getpublicationId());
+ return $publication;
+ }
+
+ /**
+ * Get the reviewer for a review assignment.
+ */
+ private function getReviewer(ReviewAssignment $reviewAssignment): User
+ {
+ if (empty($this->reviewers)) {
+ $this->loadReviewers();
+ }
+
+ /** @var User $reviewer */
+ $reviewer = $this->reviewers->get($reviewAssignment->getReviewerId());
+ return $reviewer;
+ }
+
+ /**
+ * Get the review round for a review assignment.
+ */
+ private function getReviewRound($id): ReviewRound
+ {
+ if (empty($this->reviewRounds)) {
+ $this->loadReviewRounds();
+ }
+
+ /** @var ReviewRound $reviewRound */
+ $reviewRound = $this->reviewRounds->get($id);
+
+ return $reviewRound;
+ }
+
+ /**
+ * Load the review rounds for all review assignments.
+ * @return void
+ * @throws \Exception
+ */
+ private function loadReviewRounds(): void
+ {
+ if (!empty($this->reviewRounds)) {
+ return;
+ }
+
+ /** @var ReviewRoundDAO $reviewRoundDao */
+ $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
+ $this->reviewRounds = collect();
+
+ foreach ($this->reviewAssignments as $reviewAssignment) {
+ $this->reviewRounds->put(
+ $reviewAssignment->getReviewRoundId(),
+ $reviewRoundDao->getById($reviewAssignment->getReviewRoundId())
+ );
+ }
+ }
+
+
+ /**
+ * Load the reviewers for all review assignments
+ * @return void
+ */
+ private function loadReviewers(): void{
+ if (!empty($this->reviewers)) {
+ return;
+ }
+
+ $reviewerIds = array_unique(
+ array_map(fn(ReviewAssignment $review) => $review->getReviewerId(), $this->reviewAssignments)
+ );
+
+ $this->reviewers = Repo::user()->getCollector()
+ ->filterByUserIds($reviewerIds)
+ ->getMany()
+ ->keyBy(fn(User $user)=> $user->getId());
+ }
+ /**
+ * Load the publications for all review assignments.
+ * @return void
+ */
+ private function loadPublications(): void
+ {
+ if (!empty($this->publications)) {
+ return;
+ }
+
+ $this->publications = collect();
+ $publicationIds = [];
+
+ foreach ($this->reviewAssignments as $reviewAssignment) {
+ $reviewRound = $this->getReviewRound($reviewAssignment->getReviewRoundId());
+ $publicationIds[] = $reviewRound->getPublicationId();
+ }
+
+ $publications = Repo::publication()->getCollector()
+ ->filterByPublicationIds(array_unique($publicationIds))
+ ->getMany()
+ ->keyBy(fn(Publication $publication) => $publication->getId());
+
+ $this->publications = $publications;
+ }
+}
diff --git a/filter/filterConfig.xml b/filter/filterConfig.xml
index eca5d26..38d255c 100644
--- a/filter/filterConfig.xml
+++ b/filter/filterConfig.xml
@@ -26,6 +26,13 @@
description="plugins.importexport.crossref.description"
inputType="class::APP\submission\Submission[]"
outputType="xml::schema(https://www.crossref.org/schemas/crossref5.4.0.xsd)" />
+
+
@@ -38,5 +45,10 @@
inGroup="article=>crossref-xml"
class="APP\plugins\generic\crossref\filter\ArticleCrossrefXmlFilter"
isTemplate="0" />
+
+
diff --git a/filter/trait/CrossrefFilterBuilder.php b/filter/trait/CrossrefFilterBuilder.php
new file mode 100644
index 0000000..2eaa4ce
--- /dev/null
+++ b/filter/trait/CrossrefFilterBuilder.php
@@ -0,0 +1,94 @@
+getDeployment();
+ $context = $deployment->getContext();
+ $plugin = $deployment->getPlugin();
+ $headNode = $doc->createElementNS($deployment->getNamespace(), 'head');
+ $headNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'doi_batch_id', htmlspecialchars($context->getData('acronym', $context->getPrimaryLocale()) . '_' . time(), ENT_COMPAT, 'UTF-8')));
+ $headNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'timestamp', date('YmdHisv')));
+ $depositorNode = $doc->createElementNS($deployment->getNamespace(), 'depositor');
+ $depositorName = $plugin->getSetting($context->getId(), 'depositorName');
+ if (empty($depositorName)) {
+ $depositorName = $context->getData('supportName');
+ }
+ $depositorEmail = $plugin->getSetting($context->getId(), 'depositorEmail');
+ if (empty($depositorEmail)) {
+ $depositorEmail = $context->getData('supportEmail');
+ }
+ $depositorNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'depositor_name', htmlspecialchars($depositorName, ENT_COMPAT, 'UTF-8')));
+ $depositorNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'email_address', htmlspecialchars($depositorEmail, ENT_COMPAT, 'UTF-8')));
+ $headNode->appendChild($depositorNode);
+ $publisherInstitution = $context->getData('publisherInstitution');
+ $headNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'registrant', htmlspecialchars($publisherInstitution, ENT_COMPAT, 'UTF-8')));
+ return $headNode;
+ }
+
+ /**
+ * Create and return the DOI data node 'doi_data'.
+ */
+ public function createDOIDataNode(DOMDocument $doc, string $doi, string $url): DOMElement
+ {
+ $deployment = $this->getDeployment();
+ $doiDataNode = $doc->createElementNS($deployment->getNamespace(), 'doi_data');
+ $doiDataNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'doi', htmlspecialchars($doi, ENT_COMPAT, 'UTF-8')));
+ $doiDataNode->appendChild($node = $doc->createElementNS($deployment->getNamespace(), 'resource', $url));
+ return $doiDataNode;
+ }
+
+ /**
+ * Helper to ensure dispatcher is available even when called from CLI tools
+ */
+ protected function _getDispatcher(Request $request): Dispatcher
+ {
+ $dispatcher = $request?->getDispatcher();
+ if ($dispatcher === null) {
+ $dispatcher = Application::get()->getDispatcher();
+ }
+ return $dispatcher;
+ }
+
+ /**
+ * Create and return the root node 'doi_batch'.
+ */
+ public function createRootNode(DOMDocument $doc): DOMElement
+ {
+ /** @var CrossrefExportDeployment $deployment */
+ $deployment = $this->getDeployment();
+ $rootNode = $doc->createElementNS($deployment->getNamespace(), $deployment->getRootElementName());
+ $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', $deployment->getXmlSchemaInstance());
+ $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:jats', $deployment->getJATSNamespace());
+ $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ai', $deployment->getAINamespace());
+ $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:rel', $deployment->getRelNamespace());
+ $rootNode->setAttribute('version', $deployment->getXmlSchemaVersion());
+ $rootNode->setAttribute('xsi:schemaLocation', $deployment->getNamespace() . ' ' . $deployment->getSchemaFilename());
+ return $rootNode;
+ }
+}
diff --git a/locale/en/locale.po b/locale/en/locale.po
index d9c1ff3..342034f 100644
--- a/locale/en/locale.po
+++ b/locale/en/locale.po
@@ -193,3 +193,6 @@ msgstr "The submission \"{$publicationTitle}\" is not associated with a valid DO
msgid "plugins.generic.crossref.publisherInstitution.required"
msgstr "Journal publisher must be provided before submissions can be deposited with Crossref."
+
+msgid "plugins.importexport.crossref.reviewTitle"
+msgstr "Review: {publicationTitle}"