diff --git a/classes/components/listPanels/DoiListPanel.php b/classes/components/listPanels/DoiListPanel.php index f2f82610f9d..4724fbd4f64 100644 --- a/classes/components/listPanels/DoiListPanel.php +++ b/classes/components/listPanels/DoiListPanel.php @@ -52,6 +52,15 @@ protected function setAppConfig(array &$config): void ] ] ]; - + $config['filters'][] = [ + 'heading' => __('manager.dois.filters.workflow'), + 'filters' => [ + [ + 'title' => __('manager.dois.filters.inEditingOrPublished'), + 'param' => 'inEditingOrPublished', + 'value' => '1', + ], + ], + ]; } } diff --git a/classes/doi/DAO.php b/classes/doi/DAO.php index 41f2e402489..5a27c891c63 100644 --- a/classes/doi/DAO.php +++ b/classes/doi/DAO.php @@ -18,6 +18,7 @@ namespace APP\doi; +use APP\core\Application; use APP\facades\Repo; use Illuminate\Database\Query\Builder; use Illuminate\Support\Collection; @@ -38,62 +39,66 @@ class DAO extends \PKP\doi\DAO public function getAllDepositableSubmissionIds(Context $context): Collection { $enabledDoiTypes = $context->getData(Context::SETTING_ENABLED_DOI_TYPES) ?? []; + $doiVersioning = (bool) $context->getData(Context::SETTING_DOI_VERSIONING); $q = DB::table($this->table, 'd') + // Minor versions share their major version's DOIs, so a DOI can match several publications/objects ->leftJoin('publications as p', 'd.doi_id', '=', 'p.doi_id') - ->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id') + ->leftJoin('submission_chapters as cd', 'd.doi_id', '=', 'cd.doi_id') + ->leftJoin('publications as cp', 'cd.publication_id', '=', 'cp.publication_id') + ->leftJoin('publication_formats as pfd', 'd.doi_id', '=', 'pfd.doi_id') + ->leftJoin('publications as pfp', 'pfd.publication_id', '=', 'pfp.publication_id') + ->leftJoin('submission_files as sfd', 'd.doi_id', '=', 'sfd.doi_id') ->where('d.context_id', '=', $context->getId()) - ->where(function (Builder $q) use ($enabledDoiTypes) { + ->where(function (Builder $q) use ($enabledDoiTypes, $doiVersioning) { // Publication DOIs - $q->when(in_array(Repo::doi()::TYPE_PUBLICATION, $enabledDoiTypes), function (Builder $q) { - $q->whereIn('d.doi_id', function (Builder $q) { + $q->when(in_array(Repo::doi()::TYPE_PUBLICATION, $enabledDoiTypes), function (Builder $q) use ($doiVersioning) { + $q->whereIn('d.doi_id', function (Builder $q) use ($doiVersioning) { $q->select('p.doi_id') ->from('publications', 'p') - ->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id') - ->whereColumn('p.publication_id', '=', 's.current_publication_id') ->whereNotNull('p.doi_id') ->where('p.status', '=', PKPPublication::STATUS_PUBLISHED); + $this->whereDepositablePublication($q, $doiVersioning); }); }); // Chapter DOIs - $q->when(in_array(Repo::doi()::TYPE_CHAPTER, $enabledDoiTypes), function (Builder $q) { - $q->orWhereIn('d.doi_id', function (Builder $q) { + $q->when(in_array(Repo::doi()::TYPE_CHAPTER, $enabledDoiTypes), function (Builder $q) use ($doiVersioning) { + $q->orWhereIn('d.doi_id', function (Builder $q) use ($doiVersioning) { $q->select('spc.doi_id') ->from('submission_chapters', 'spc') ->join('publications as p', 'spc.publication_id', '=', 'p.publication_id') - ->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id') - ->whereColumn('p.publication_id', '=', 's.current_publication_id') ->whereNotNull('spc.doi_id') ->where('p.status', '=', PKPPublication::STATUS_PUBLISHED); + $this->whereDepositablePublication($q, $doiVersioning, false); }); }); // Publication format DOIs - $q->when(in_array(Repo::doi()::TYPE_REPRESENTATION, $enabledDoiTypes), function (Builder $q) { - $q->orWhereIn('d.doi_id', function (Builder $q) { + $q->when(in_array(Repo::doi()::TYPE_REPRESENTATION, $enabledDoiTypes), function (Builder $q) use ($doiVersioning) { + $q->orWhereIn('d.doi_id', function (Builder $q) use ($doiVersioning) { $q->select('pf.doi_id') ->from('publication_formats', 'pf') ->join('publications as p', 'pf.publication_id', '=', 'p.publication_id') - ->leftJoin('submissions as s', 'p.publication_id', '=', 's.current_publication_id') - ->whereColumn('p.publication_id', '=', 's.current_publication_id') ->whereNotNull('pf.doi_id') ->where('p.status', '=', PKPPublication::STATUS_PUBLISHED); + $this->whereDepositablePublication($q, $doiVersioning, false); }); }); - // Submission file DOIs - $q->when(in_array(Repo::doi()::TYPE_SUBMISSION_FILE, $enabledDoiTypes), function (Builder $q) { - $q->orWhereIn('d.doi_id', function (Builder $q) { + // Submission file DOIs, of the proof files of a publication format + $q->when(in_array(Repo::doi()::TYPE_SUBMISSION_FILE, $enabledDoiTypes), function (Builder $q) use ($doiVersioning) { + $q->orWhereIn('d.doi_id', function (Builder $q) use ($doiVersioning) { $q->select('sf.doi_id') ->from('submission_files', 'sf') - ->join('submissions as s', 's.submission_id', '=', 'sf.submission_id') - ->leftJoin('publications as p', 's.current_publication_id', '=', 'p.publication_id') - ->whereColumn('p.publication_id', '=', 's.current_publication_id') + ->join('publication_formats as pf', 'sf.assoc_id', '=', 'pf.publication_format_id') + ->join('publications as p', 'pf.publication_id', '=', 'p.publication_id') + ->where('sf.assoc_type', '=', Application::ASSOC_TYPE_PUBLICATION_FORMAT) ->where('sf.file_stage', '=', SubmissionFile::SUBMISSION_FILE_PROOF) ->whereNotNull('sf.doi_id') ->where('p.status', '=', PKPPublication::STATUS_PUBLISHED); + $this->whereDepositablePublication($q, $doiVersioning, false); }); }); }) ->whereIn('d.status', [Doi::STATUS_UNREGISTERED, Doi::STATUS_ERROR, Doi::STATUS_STALE]); - return $q->get(['s.submission_id', 'd.doi_id']); + return $q->distinct()->get([DB::raw('COALESCE(p.submission_id, cp.submission_id, pfp.submission_id, sfd.submission_id) AS submission_id'), 'd.doi_id']); } } diff --git a/classes/doi/Repository.php b/classes/doi/Repository.php index b856233af3c..706a2bfb0e4 100644 --- a/classes/doi/Repository.php +++ b/classes/doi/Repository.php @@ -113,61 +113,12 @@ public function mintSubmissionFileDoi(SubmissionFile $submissionFile, Submission public function getDoisForSubmission(int $submissionId): array { - $doiIds = Collection::make(); - $submission = Repo::submission()->get($submissionId); - /** @var Publication[] $publications */ - $publications = [$submission->getCurrentPublication()]; - - /** @var PressDAO $contextDao */ - $contextDao = Application::getContextDAO(); - /** @var Press */ - $context = $contextDao->getById($submission->getData('contextId')); - - foreach ($publications as $publication) { - $publicationDoiId = $publication->getData('doiId'); - if (!empty($publicationDoiId) && $context->isDoiTypeEnabled(self::TYPE_PUBLICATION)) { - $doiIds->add($publicationDoiId); - } - - // Chapters - $chapters = $publication->getData('chapters'); - foreach ($chapters as $chapter) { - $chapterDoiId = $chapter->getData('doiId'); - if (!empty($chapterDoiId) && $context->isDoiTypeEnabled(self::TYPE_CHAPTER)) { - $doiIds->add($chapterDoiId); - } - } - - // Publication formats - $publicationFormats = $publication->getData('publicationFormats'); - foreach ($publicationFormats as $publicationFormat) { - $publicationFormatDoiId - = $publicationFormat->getData('doiId'); - if (!empty($publicationFormatDoiId) && $context->isDoiTypeEnabled(self::TYPE_REPRESENTATION)) { - $doiIds->add($publicationFormatDoiId); - } - } - - // Submission files - if ($context->isDoiTypeEnabled(self::TYPE_SUBMISSION_FILE)) { - $submissionFiles = Repo::submissionFile() - ->getCollector() - ->filterBySubmissionIds([$publication->getData('submissionId')]) - ->filterByFileStages([SubmissionFile::SUBMISSION_FILE_PROOF]) - ->getMany(); - - /** @var SubmissionFile $submissionFile */ - foreach ($submissionFiles as $submissionFile) { - $submissionFileDoiId = $submissionFile->getData('doiId'); - if (!empty($submissionFileDoiId)) { - $doiIds->add($submissionFileDoiId); - } - } - } - } - - return $doiIds->unique()->toArray(); + return collect($submission->getData('publications')) + ->flatMap(fn (Publication $publication) => $this->getDoisForPublication($publication)) + ->unique() + ->values() + ->all(); } /** @@ -212,10 +163,13 @@ public function getDoisForPublication(Publication $publication): array // Submission files if ($context->isDoiTypeEnabled(self::TYPE_SUBMISSION_FILE)) { - $submissionFiles = Repo::submissionFile() + // Only the files of this version's publication formats + $publicationFormatIds = collect($publicationFormats)->map(fn (PublicationFormat $publicationFormat) => $publicationFormat->getId())->all(); + $submissionFiles = empty($publicationFormatIds) ? [] : Repo::submissionFile() ->getCollector() ->filterBySubmissionIds([$publication->getData('submissionId')]) ->filterByFileStages([SubmissionFile::SUBMISSION_FILE_PROOF]) + ->filterByAssoc(Application::ASSOC_TYPE_PUBLICATION_FORMAT, $publicationFormatIds) ->getMany(); /** @var SubmissionFile $submissionFile */ diff --git a/classes/publication/DAO.php b/classes/publication/DAO.php index 41a272c0765..08dabe6fbb0 100644 --- a/classes/publication/DAO.php +++ b/classes/publication/DAO.php @@ -16,8 +16,11 @@ use APP\core\Application; use APP\monograph\ChapterDAO; +use Illuminate\Database\Query\Builder; +use Illuminate\Support\Facades\DB; use PKP\core\interfaces\CollectorInterface; use PKP\db\DAORegistry; +use PKP\submissionFile\SubmissionFile; class DAO extends \PKP\publication\DAO { @@ -55,4 +58,34 @@ public function fromRow(object $row, array $ids, object $cache, ?CollectorInterf $publication->setData('chapters', $chapterDao->getByPublicationId($publication->getId())->toArray()); return $publication; } + + /** + * @copydoc \PKP\publication\DAO::whereHasDoi() + */ + protected function whereHasDoi(Builder $q): Builder + { + return parent::whereHasDoi($q) + ->orWhereExists( + fn (Builder $q) => $q->select(DB::raw(1)) + ->from('submission_chapters as spc') + ->whereColumn('spc.publication_id', '=', 'p.publication_id') + ->whereNotNull('spc.doi_id') + ) + ->orWhereExists( + fn (Builder $q) => $q->select(DB::raw(1)) + ->from('publication_formats as pf') + ->whereColumn('pf.publication_id', '=', 'p.publication_id') + ->where( + fn (Builder $q) => $q->whereNotNull('pf.doi_id') + ->orWhereExists( + fn (Builder $q) => $q->select(DB::raw(1)) + ->from('submission_files as sf') + ->where('sf.assoc_type', '=', Application::ASSOC_TYPE_PUBLICATION_FORMAT) + ->whereColumn('sf.assoc_id', '=', 'pf.publication_format_id') + ->where('sf.file_stage', '=', SubmissionFile::SUBMISSION_FILE_PROOF) + ->whereNotNull('sf.doi_id') + ) + ) + ); + } } diff --git a/classes/publication/Repository.php b/classes/publication/Repository.php index dfc042d93fb..67e7671993c 100644 --- a/classes/publication/Repository.php +++ b/classes/publication/Repository.php @@ -38,6 +38,7 @@ use PKP\db\DAORegistry; use PKP\doi\exceptions\DoiException; use PKP\notification\Notification; +use PKP\observers\events\PublicationVersioned; use PKP\plugins\Hook; use PKP\publication\Collector; use PKP\publication\PKPPublication; @@ -252,6 +253,8 @@ public function version(Publication $publication, ?VersionStage $versionStage = } } + event(new PublicationVersioned($this->get($newId), $publication, Repo::submission()->get($submissionId), $context)); + return $newId; } @@ -483,6 +486,24 @@ public function makeThumbnail($filePath, $thumbFileName, $maxWidth, $maxHeight) imagedestroy($thumb); } + /** + * Assign DOIs to a publication's new chapters, formats or files, if DOIs are assigned on item creation + */ + public function createDoisOnCreation(int $publicationId): void + { + $publication = $this->get($publicationId); + $submission = Repo::submission()->get($publication->getData('submissionId')); + // Objects added during the submission wizard get their DOIs on submit + if ($submission->getData('submissionProgress')) { + return; + } + + $context = Application::getContextDAO()->getById($submission->getData('contextId')); + if (Repo::doi()->assignOnItemCreation($context)) { + $this->createDois($publication); + } + } + /** @copydoc \PKP\publication\Repository::createDois() */ public function createDois(Publication $publication): array { @@ -544,11 +565,13 @@ public function createDois(Publication $publication): array // Submission files if ($context->isDoiTypeEnabled(Repo::doi()::TYPE_SUBMISSION_FILE)) { - // Get all submission files assigned to a publication format - $submissionFiles = Repo::submissionFile() + // Only the files of this version's publication formats + $publicationFormatIds = collect($publicationFormats)->map(fn (PublicationFormat $publicationFormat) => $publicationFormat->getId())->all(); + $submissionFiles = empty($publicationFormatIds) ? [] : Repo::submissionFile() ->getCollector() ->filterBySubmissionIds([$publication->getData('submissionId')]) ->filterByFileStages([SubmissionFile::SUBMISSION_FILE_PROOF]) + ->filterByAssoc(Application::ASSOC_TYPE_PUBLICATION_FORMAT, $publicationFormatIds) ->getMany(); /** @var SubmissionFile $submissionFile */ diff --git a/classes/submission/Collector.php b/classes/submission/Collector.php index 67962e32507..cb6cb5acbc3 100644 --- a/classes/submission/Collector.php +++ b/classes/submission/Collector.php @@ -131,12 +131,27 @@ protected function addHasDoisFilterToQuery(Builder $q) /** * APP-specific filtering for submissions that should be listed on the DOI management page. - * Those are: - * submissions in the workflow editing and production stage, - * submissions that have a published publication, and - * submissions whose sub objects have a DOI. + * Those are all submitted submissions that have not been declined, and all submissions in the + * copyediting or production stage, with a published publication or with a DOI. The latter + * can also be listed on their own (inEditingOrPublished). */ protected function addOnDoiPageFilterToQuery(Builder $q) + { + $q->where('s.submission_progress', ''); + if ($this->inEditingOrPublished) { + $this->addInEditingPublishedOrWithDoisToQuery($q); + } else { + $q->where(function (Builder $q) { + $q->where('s.status', '!=', Submission::STATUS_DECLINED) + ->orWhere(fn (Builder $q) => $this->addInEditingPublishedOrWithDoisToQuery($q)); + }); + } + } + + /** + * Submissions in the copyediting or production stage, with a published publication or with a DOI + */ + protected function addInEditingPublishedOrWithDoisToQuery(Builder $q): void { $q->where(function (Builder $q) { $q->whereIn('s.stage_id', [WORKFLOW_STAGE_ID_EDITING, WORKFLOW_STAGE_ID_PRODUCTION]) diff --git a/classes/submission/Repository.php b/classes/submission/Repository.php index 8dc347743dd..b2860beaec0 100644 --- a/classes/submission/Repository.php +++ b/classes/submission/Repository.php @@ -116,11 +116,13 @@ public function createDois(Submission $submission): array // Submission files if ($context->isDoiTypeEnabled(Repo::doi()::TYPE_SUBMISSION_FILE)) { - // Get all submission files assigned to a publication format - $submissionFiles = Repo::submissionFile() + // Only the files of this version's publication formats + $publicationFormatIds = collect($publicationFormats)->map(fn (PublicationFormat $publicationFormat) => $publicationFormat->getId())->all(); + $submissionFiles = empty($publicationFormatIds) ? [] : Repo::submissionFile() ->getCollector() ->filterBySubmissionIds([$publication->getData('submissionId')]) ->filterByFileStages([SubmissionFile::SUBMISSION_FILE_PROOF]) + ->filterByAssoc(Application::ASSOC_TYPE_PUBLICATION_FORMAT, $publicationFormatIds) ->getMany(); /** @var SubmissionFile $submissionFile */ diff --git a/classes/submissionFile/Repository.php b/classes/submissionFile/Repository.php index 3ecb201efde..a7a7e42013e 100644 --- a/classes/submissionFile/Repository.php +++ b/classes/submissionFile/Repository.php @@ -53,6 +53,28 @@ public function getCollector(): Collector return App::makeWith(Collector::class, ['dao' => $this->dao]); } + /** @copydoc SubmissionFileRepository::add() */ + public function add(SubmissionFile $submissionFile): int + { + $submissionFileId = parent::add($submissionFile); + + // Only proof files of a publication format get a DOI + if ( + !$submissionFile->getData('doiId') + && $submissionFile->getData('fileStage') == SubmissionFile::SUBMISSION_FILE_PROOF + && $submissionFile->getData('assocType') == Application::ASSOC_TYPE_PUBLICATION_FORMAT + ) { + $publicationId = DB::table('publication_formats') + ->where('publication_format_id', '=', $submissionFile->getData('assocId')) + ->value('publication_id'); + if ($publicationId) { + Repo::publication()->createDoisOnCreation($publicationId); + } + } + + return $submissionFileId; + } + public function getFileStages(): array { $stages = [ diff --git a/controllers/grid/catalogEntry/form/PublicationFormatForm.php b/controllers/grid/catalogEntry/form/PublicationFormatForm.php index a10006dbd49..cdc4115f1e8 100644 --- a/controllers/grid/catalogEntry/form/PublicationFormatForm.php +++ b/controllers/grid/catalogEntry/form/PublicationFormatForm.php @@ -284,6 +284,8 @@ public function execute(...$functionParams) 'publicationFormatName' => $publicationFormat->getData('name') ]); Repo::eventLog()->add($logEntry); + + Repo::publication()->createDoisOnCreation($this->getPublication()->getId()); } return $representationId; diff --git a/controllers/grid/users/chapter/form/ChapterForm.php b/controllers/grid/users/chapter/form/ChapterForm.php index eea75f7b27d..67c72e7153b 100644 --- a/controllers/grid/users/chapter/form/ChapterForm.php +++ b/controllers/grid/users/chapter/form/ChapterForm.php @@ -347,6 +347,9 @@ public function execute(...$functionParams) $this->getChapter()->getId() ); + // Also when its page is enabled later, as only chapters with a page get a DOI + Repo::publication()->createDoisOnCreation($this->getPublication()->getId()); + return true; } } diff --git a/cypress/tests/integration/Doi.cy.js b/cypress/tests/integration/Doi.cy.js index 648240f00c1..329c5b2a22e 100644 --- a/cypress/tests/integration/Doi.cy.js +++ b/cypress/tests/integration/Doi.cy.js @@ -26,6 +26,14 @@ describe('DOI tests', function() { cy.get('button#submission-doi-management-button').click(); }; + // The submissions the DOI page listed before it showed all submitted submissions + const filterInEditingPublishedOrWithDois = () => { + cy.get( + '#submission-doi-management button:contains("In Copyediting, Production, Published or with DOIs")' + ).click(); + cy.get('#submission-doi-management .pkpSpinner').should('not.exist'); + }; + const clearFilter = () => { cy.get('#submission-doi-management button:contains("Clear filter")').each( ($el, index, $list) => { @@ -79,9 +87,11 @@ describe('DOI tests', function() { it('Check filters and mark registered', function() { cy.log('Check Submission Filter Behaviour (pre-deposit)'); loginAndGoToDoiPage(); + filterInEditingPublishedOrWithDois(); cy.checkDoiFilterResults('Needs DOI', 'Allan — Bomb Canada and Other Unkind Remarks in the American Media', 6); cy.checkDoiFilterResults('DOI Assigned', 'Dawson et al. — From Bricks to Brains: The Embodied Cognitive Science of LEGO Robots', 1); clearFilter(); + filterInEditingPublishedOrWithDois(); cy.checkDoiFilterResults('Unregistered', 'Dawson et al. — From Bricks to Brains: The Embodied Cognitive Science of LEGO Robots', 1); clearFilter(); diff --git a/dbscripts/xml/upgrade.xml b/dbscripts/xml/upgrade.xml index 83f64b4cc47..0f0d4e39c70 100644 --- a/dbscripts/xml/upgrade.xml +++ b/dbscripts/xml/upgrade.xml @@ -235,6 +235,9 @@ + + + diff --git a/lib/pkp b/lib/pkp index 03b9a78d8f8..58c7f454ff8 160000 --- a/lib/pkp +++ b/lib/pkp @@ -1 +1 @@ -Subproject commit 03b9a78d8f83f054db1ffbb4d82b8870566edafc +Subproject commit 58c7f454ff88baf7dc6df9cd0c9090fb97364b56 diff --git a/locale/en/manager.po b/locale/en/manager.po index bb9b593c018..1de71833e32 100644 --- a/locale/en/manager.po +++ b/locale/en/manager.po @@ -412,6 +412,12 @@ msgstr "Chapters without a landing page cannot have a DOI." msgid "manager.dois.formatIdentifier.file" msgstr "Format / {$format}" +msgid "manager.dois.filters.workflow" +msgstr "Workflow" + +msgid "manager.dois.filters.inEditingOrPublished" +msgstr "In Copyediting, Production, Published or with DOIs" + msgid "manager.setup.editorDecision" msgstr "Editor Decision"