diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index b59d5360658..9b74c9e03ee 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -54,6 +54,8 @@ use PKP\notification\NotificationSubscriptionSettingsDAO; use PKP\plugins\Hook; use PKP\plugins\PluginRegistry; +use PKP\publication\enums\VersionStage; +use PKP\publication\helpers\VersionDataResource; use PKP\security\authorization\ContextAccessPolicy; use PKP\security\authorization\DecisionWritePolicy; use PKP\security\authorization\internal\SubmissionCompletePolicy; @@ -112,7 +114,9 @@ class PKPSubmissionController extends PKPBaseController 'getPublicationIdentifierForm', 'getPublicationLicenseForm', 'getPublicationTitleAbstractForm', - 'getChangeLanguageMetadata' + 'getChangeLanguageMetadata', + 'changeVersionData', + 'getNextAvailableVersionData', ]; /** @var array Handlers that must be authorized to write to a publication */ @@ -123,6 +127,7 @@ class PKPSubmissionController extends PKPBaseController 'editContributor', 'saveContributorsOrder', 'changeLocale', + 'changeVersionData' ]; /** @var array Handlers that must be authorized to access a submission's production stage */ @@ -237,6 +242,14 @@ public function getGroupRoutes(): void Route::put('{submissionId}/publications/{publicationId}/changeLocale', $this->changeLocale(...)) ->name('submission.publication.changeLocale') ->whereNumber(['submissionId', 'publicationId']); + + Route::put('{submissionId}/publications/{publicationId}/versionData', $this->changeVersionData(...)) + ->name('submission.publication.versionData') + ->whereNumber(['submissionId', 'publicationId']); + + Route::get('{submissionId}/getNextAvailableVersionData', $this->getNextAvailableVersionData(...)) + ->name('submission.getNextAvailableVersionData') + ->whereNumber(['submissionId']); }); Route::middleware([ @@ -944,6 +957,65 @@ public function changeLocale(Request $illuminateRequest): JsonResponse return $this->edit($illuminateRequest); } + /** + * Change version data for publication + */ + public function changeVersionData(Request $illuminateRequest): JsonResponse + { + $request = $this->getRequest(); + $publication = Repo::publication()->get((int) $illuminateRequest->route('publicationId')); + + if (!$publication) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION); + + if ($submission->getId() !== $publication->getData('submissionId')) { + return response()->json([ + 'error' => __('api.publications.403.submissionsDidNotMatch'), + ], Response::HTTP_FORBIDDEN); + } + + $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_PUBLICATION, $illuminateRequest->input()); + + $submissionContext = $request->getContext(); + + $errors = Repo::publication()->validate($publication, $params, $submission, $submissionContext); + + if (!empty($errors)) { + return response()->json($errors, Response::HTTP_BAD_REQUEST); + } + + $versionStage = $params['versionStage']; + $versionStageIsMinor = (bool) $params['versionIsMinor']; + + Repo::publication()->updateVersionData($publication, VersionStage::from($versionStage), $versionStageIsMinor); + + return $this->edit($illuminateRequest); + } + + /** + * Get next potential version for submission + */ + public function getNextAvailableVersionData(Request $illuminateRequest): JsonResponse + { + $submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION); + + $params = $illuminateRequest->input(); + $potentialVersionStage = VersionStage::from($params['versionStage']); + $potentialIsMinor = ($params['versionIsMinor'] === 'false') ? false : (bool) $params['versionIsMinor']; + + $potentialVersionStage = Repo::submission()->getNextAvailableVersionData($submission, $potentialVersionStage, $potentialIsMinor); + + return response()->json( + (new VersionDataResource($potentialVersionStage))->toArray($illuminateRequest), + Response::HTTP_OK + ); + } + /** * Get the decisions recorded on a submission */ diff --git a/classes/migration/upgrade/v3_5_0/I4860_MigratePublicationVersion.php b/classes/migration/upgrade/v3_5_0/I4860_MigratePublicationVersion.php new file mode 100644 index 00000000000..9ddfb5c5710 --- /dev/null +++ b/classes/migration/upgrade/v3_5_0/I4860_MigratePublicationVersion.php @@ -0,0 +1,76 @@ +enum('version_stage', array_column(VersionStage::cases(), 'value')) + ->nullable(); + + // Adding version_minor and version_major as integers + $table->integer('version_minor') + ->nullable(); + + $table->integer('version_major') + ->nullable(); + }); + + // Update the version_major column based on the version column + DB::table('publications')->update([ + 'version_major' => DB::raw('version'), // Copy version to version_major + 'version_minor' => 0, // Set version_minor to 0 + 'version_stage' => VersionStage::VERSION_OF_RECORD, // Set version_stage to VERSION_OF_RECORD + ]); + + // remove the `version` column + Schema::table('publications', function (Blueprint $table) { + $table->dropColumn('version'); + }); + } + + /** + * Reverses the migration + */ + public function down(): void + { + // Re-add the `version` column + Schema::table('publications', function (Blueprint $table) { + $table->integer('version')->nullable(); + }); + + // Update the `version` column based on `version_major` + DB::table('publications')->update([ + 'version' => DB::raw('version_major') + ]); + + // Drop the added columns + Schema::table('publications', function (Blueprint $table) { + $table->dropColumn(['version_stage', 'version_minor', 'version_major']); + }); + } +} diff --git a/classes/publication/Collector.php b/classes/publication/Collector.php index 856d3a54e5b..1a597412d84 100644 --- a/classes/publication/Collector.php +++ b/classes/publication/Collector.php @@ -134,7 +134,7 @@ public function getQueryBuilder(): Builder $qb->offset($this->offset); } - $qb->orderBy('p.version', 'asc'); + $qb->orderBy('p.publication_id', 'asc'); // Add app-specific query statements Hook::call('Publication::Collector', [&$qb, $this]); diff --git a/classes/publication/PKPPublication.php b/classes/publication/PKPPublication.php index 24f523719c4..4ec26a05d07 100644 --- a/classes/publication/PKPPublication.php +++ b/classes/publication/PKPPublication.php @@ -23,6 +23,8 @@ use PKP\core\Core; use PKP\core\PKPString; use PKP\facades\Locale; +use PKP\publication\enums\VersionStage; +use PKP\publication\helpers\VersionData; use PKP\services\PKPSchemaService; use PKP\userGroup\UserGroup; @@ -466,6 +468,35 @@ public function getLanguages(?array ...$additionalLocales): array ->values() ->toArray(); } + + /** + * Get the current version data + */ + public function getCurrentVersionData(): ?VersionData + { + $versionStageStr = $this->getData('versionStage'); + if (!isset($versionStageStr)) { + return null; + } + + $versionStage = new VersionData(); + $versionStage->stage = VersionStage::from($versionStageStr); + $versionStage->majorNumbering = $this->getData('versionMajor'); + $versionStage->minorNumbering = $this->getData('versionMinor'); + + return $versionStage; + } + + /** + * Set the current version of the publication + * given a VersionData object + */ + public function setVersionData(VersionData $versionData): void + { + $this->setData('versionStage', $versionData->stage->value); + $this->setData('versionMajor', $versionData->majorNumbering); + $this->setData('versionMinor', $versionData->minorNumbering); + } } if (!PKP_STRICT_MODE) { class_alias('\PKP\publication\PKPPublication', '\PKPPublication'); diff --git a/classes/publication/Repository.php b/classes/publication/Repository.php index 69a04a52af3..20c4de18798 100644 --- a/classes/publication/Repository.php +++ b/classes/publication/Repository.php @@ -24,13 +24,16 @@ use PKP\context\Context; use PKP\core\Core; use PKP\core\PKPApplication; +use PKP\core\PKPString; use PKP\db\DAORegistry; +use PKP\facades\Locale; use PKP\file\TemporaryFileManager; use PKP\log\event\PKPSubmissionEventLogEntry; use PKP\observers\events\PublicationPublished; use PKP\observers\events\PublicationUnpublished; use PKP\orcid\OrcidManager; use PKP\plugins\Hook; +use PKP\publication\enums\VersionStage; use PKP\security\Validation; use PKP\services\PKPSchemaService; use PKP\submission\Genre; @@ -327,13 +330,31 @@ public function add(Publication $publication): int * * @hook Publication::version [[&$newPublication, $publication]] */ - public function version(Publication $publication): int + public function version(Publication $publication, ?VersionStage $versionStage = null, bool $isMinorVersion = true): int { $newPublication = clone $publication; $newPublication->setData('id', null); $newPublication->setData('datePublished', null); $newPublication->setData('status', Submission::STATUS_QUEUED); - $newPublication->setData('version', $publication->getData('version') + 1); + + $submission = Repo::submission()->get($publication->getData('submissionId')); + + // VersionStage Update + $newVersionStage = $versionStage; + $newIsMinorVersion = $isMinorVersion; + + if (!isset($newVersionStage)) { + $currentVersionData = $newPublication->getCurrentVersionData(); + if (isset($currentVersionData)) { + $newVersionStage = $currentVersionData->stage; + } + } + + if (isset($newVersionStage)) { + $newVersionStage = Repo::submission()->getNextAvailableVersionData($submission, $newVersionStage, $newIsMinorVersion); + $newPublication->setVersionData($newVersionStage); + } + $newPublication->stampModified(); $request = Application::get()->getRequest(); @@ -379,8 +400,6 @@ public function version(Publication $publication): int Hook::call('Publication::version', [&$newPublication, $publication]); - $submission = Repo::submission()->get($newPublication->getData('submissionId')); - $eventLog = Repo::eventLog()->newDataObject([ 'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION, 'assocId' => $submission->getId(), @@ -500,6 +519,12 @@ public function publish(Publication $publication) ); } + // Update publication version data + $currentVersionData = $newPublication->getCurrentVersionData(); + if (!isset($currentVersionData)) { + $this->updateVersionData($newPublication, VersionStage::VERSION_OF_RECORD, false); + } + Hook::call('Publication::publish::before', [&$newPublication, $publication]); $this->dao->update($newPublication); @@ -661,6 +686,66 @@ public function delete(Publication $publication) Hook::call('Publication::delete', [&$publication]); } + /** + * Given a Version Stage and a flag of whether the Version isMinor, + * the publication's related data is being updated + * + * @hook 'Publication::updateVersionData::before' [[ &$newPublication, $publication ]] + */ + public function updateVersionData(Publication $publication, VersionStage $versioningStage, bool $isMinor = true): Publication + { + $submission = Repo::submission()->get($publication->getData('submissionId')); + $nextAvailableVersionStage = Repo::submission()->getNextAvailableVersionData($submission, $versioningStage, $isMinor); + + $newPublication = clone $publication; + $newPublication->setData('versionIsMinor', $isMinor); + $newPublication->setVersionData($nextAvailableVersionStage); + + $newPublication->stampModified(); + Hook::call( + 'Publication::updateVersionData::before', + [ + &$newPublication, + $publication + ] + ); + + $this->dao->update($newPublication); + + $newPublication = Repo::publication()->get($newPublication->getId()); + + return $newPublication; + } + + /** + * Get the string that describes the + * given publication's version. + */ + public function getVersionDataDisplay(Publication $publication, ?Submission $submission = null, ?Context $submissionContext = null): string + { + $currentVersionStage = $publication->getCurrentVersionData(); + + if (!isset($currentVersionStage)) { + if (!isset($submissionContext)) { + if (!isset($submission)) { + $submission = Repo::submission()->get($publication->getData('submissionId')); + } + + $submissionContext = app()->get('context')->get($submission->getData('contextId')); + } + + $dateFormatShort = PKPString::convertStrftimeFormat($submissionContext->getLocalizedDateFormatShort()); + + return __('publication.versionStage.unassignedVersion', [ + 'publicationCreatedDate' => (new \Carbon\Carbon($publication->getData('lastModified'))) + ->locale(Locale::getLocale()) + ->translatedFormat($dateFormatShort), + ]); + } + + return $currentVersionStage->display(); + } + /** * Handle a publication setting for an uploaded file * diff --git a/classes/publication/enums/VersionStage.php b/classes/publication/enums/VersionStage.php new file mode 100644 index 00000000000..3e0468b9d50 --- /dev/null +++ b/classes/publication/enums/VersionStage.php @@ -0,0 +1,37 @@ + __('publication.versionStage.authorOriginal'), + self::ACCEPTED_MANUSCRIPT => __('publication.versionStage.acceptedManuscript'), + self::SUBMITTED_MANUSCRIPT => __('publication.versionStage.submittedManuscript'), + self::PROOF => __('publication.versionStage.proof'), + self::VERSION_OF_RECORD => __('publication.versionStage.versionOfRecord'), + }; + } +} diff --git a/classes/publication/helpers/VersionData.php b/classes/publication/helpers/VersionData.php new file mode 100644 index 00000000000..31e6cf2d183 --- /dev/null +++ b/classes/publication/helpers/VersionData.php @@ -0,0 +1,49 @@ +stage->label(); + + return __('publication.versionStage.display', [ + 'stage' => $versionStageLabel, + 'majorNumbering' => $this->majorNumbering, + 'minorNumbering' => $this->minorNumbering, + ]); + } + + public static function createDefaultForStage(VersionStage $versionStage): VersionData + { + $defaultVersionStage = new VersionData(); + $defaultVersionStage->stage = $versionStage; + $defaultVersionStage->majorNumbering = VersionData::DEFAULT_MAJOR_NUMBERING; + $defaultVersionStage->minorNumbering = VersionData::DEFAULT_MINOR_NUMBERING; + + return $defaultVersionStage; + } +} + diff --git a/classes/publication/helpers/VersionDataResource.php b/classes/publication/helpers/VersionDataResource.php new file mode 100644 index 00000000000..317f3e8b461 --- /dev/null +++ b/classes/publication/helpers/VersionDataResource.php @@ -0,0 +1,35 @@ + $this->stage->value, + 'stageLabel' => $this->stage->label(), + 'majorNumbering' => $this->majorNumbering, + 'minorNumbering' => $this->minorNumbering, + 'display' => $this->display(), + ]; + } +} + diff --git a/classes/publication/maps/Schema.php b/classes/publication/maps/Schema.php index 5b56bfb1f25..8c67606e3b6 100644 --- a/classes/publication/maps/Schema.php +++ b/classes/publication/maps/Schema.php @@ -155,6 +155,9 @@ function ($citation) { case 'fullTitle': $output[$prop] = $publication->getFullTitles('html'); break; + case 'versionDataDisplay': + $output[$prop] = Repo::publication()->getVersionDataDisplay($publication); + break; default: $output[$prop] = $publication->getData($prop); break; diff --git a/classes/submission/PKPSubmission.php b/classes/submission/PKPSubmission.php index 7c7985a38e3..1d3f804cdbf 100644 --- a/classes/submission/PKPSubmission.php +++ b/classes/submission/PKPSubmission.php @@ -29,10 +29,12 @@ use APP\facades\Repo; use APP\publication\Publication; use APP\submission\DAO; +use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; use PKP\core\Core; use PKP\facades\Locale; -use PKP\i18n\LocaleMetadata; +use PKP\publication\enums\VersionStage; +use PKP\publication\helpers\VersionData; /** * @extends \PKP\core\DataObject diff --git a/classes/submission/Repository.php b/classes/submission/Repository.php index 64219b7f9cf..4327f4a4b97 100644 --- a/classes/submission/Repository.php +++ b/classes/submission/Repository.php @@ -32,6 +32,8 @@ use PKP\facades\Locale; use PKP\observers\events\SubmissionSubmitted; use PKP\plugins\Hook; +use PKP\publication\enums\VersionStage; +use PKP\publication\helpers\VersionData; use PKP\security\Role; use PKP\security\RoleDAO; use PKP\services\PKPSchemaService; @@ -570,7 +572,7 @@ public function add(Submission $submission, Publication $publication, Context $c $submission = Repo::submission()->get($submissionId); $publication->setData('submissionId', $submission->getId()); - $publication->setData('version', 1); + if (!$publication->getData('status')) { $publication->setData('status', $submission->getData('status')); } @@ -837,6 +839,51 @@ public function getDashboardViews(Context $context, User $user, array $selectedR return $filteredViews; } + /** + * Return a collection of all existing Version Data + * for the submission's publications + * + * @return Collection + */ + public function getAllVersionDataByPublications(Submission $submission): Collection + { + return collect($submission->getData('publications')->map(function ($publication) { + return $publication->getCurrentVersionData(); + })->filter()); // Remove any null entries from the collection + } + + public function getNextAvailableVersionData(Submission $submission, VersionStage $versioningStage, bool $isMinorChange = true): VersionData + { + $nextVersionStage = VersionData::createDefaultForStage($versioningStage); + + $submissionVersionStages = $this->getAllVersionDataByPublications($submission); + + // Filter to find the version stages that match the current versioningStage but with potentially updated versionMajor or versionMinor + $matchingVersionStages = $submissionVersionStages->filter(function ($existingVersionStage) use ($versioningStage) { + return $existingVersionStage->stage === $versioningStage; + }); + + if (!$matchingVersionStages->isEmpty()) { + // Sort the version stages by versionMajor and versionMinor (descending order) + $sortedVersionStages = $matchingVersionStages->sort(function ($a, $b) { + return $b->majorNumbering <=> $a->majorNumbering ?: $b->minorNumbering <=> $a->minorNumbering; + }); + + $nextVersionStage = $sortedVersionStages->first(); + + if ($isMinorChange) { + // Increment the minor version + $nextVersionStage->minorNumbering += 1; + } else { + // Increment the major version and reset the minor version + $nextVersionStage->majorNumbering += 1; + $nextVersionStage->minorNumbering = VersionData::DEFAULT_MINOR_NUMBERING; + } + } + + return $nextVersionStage; + } + /** * Returns a Collection of mapped dashboard views */ diff --git a/locale/en/submission.po b/locale/en/submission.po index 3bd38c49666..606cd7ccdfa 100644 --- a/locale/en/submission.po +++ b/locale/en/submission.po @@ -2350,8 +2350,9 @@ msgstr "You must upload at least one {$genre} file." msgid "submission.files.required.genres" msgstr "You must upload at least one of each of the following file types: {$genres}." +# fuzzy msgid "publication.version" -msgstr "Version {$version}" +msgstr "{$version}" msgid "submission.submissionFile" msgstr "Submission File" @@ -2658,3 +2659,35 @@ msgstr "Select incomplete submissions to be deleted." msgid "dashboard.submissions.incomplete.bulkDelete.button" msgstr "Delete Incomplete Submissions" +msgid "publication.versionStage.authorOriginal" +msgstr "Author Original" + +msgid "publication.versionStage.acceptedManuscript" +msgstr "Accepted Manuscript" + +msgid "publication.versionStage.submittedManuscript" +msgstr "Submitted Manuscript" + +msgid "publication.versionStage.proof" +msgstr "Proof" + +msgid "publication.versionStage.versionOfRecord" +msgstr "Version of Record" + +msgid "publication.versionStage.unassignedVersion" +msgstr "unassigned version ({$publicationCreatedDate})" + +msgid "publication.versionStage.display" +msgstr "{$stage} {$majorNumbering}.{$minorNumbering}" + +msgid "publication.required.versionStage" +msgstr "" +"The publication must have a version stage assigned before it can " +"be published." + +msgid "publication.required.versionStage.assignment" +msgstr "The stage version that will be assigned to the publication is '{$versionDataDisplay}'" + +msgid "publication.required.versionStage.alreadyAssignment" +msgstr "The publication version is '{$versionDataDisplay}'" + diff --git a/pages/authorDashboard/PKPAuthorDashboardHandler.php b/pages/authorDashboard/PKPAuthorDashboardHandler.php index c65036023d9..5ded0551a56 100644 --- a/pages/authorDashboard/PKPAuthorDashboardHandler.php +++ b/pages/authorDashboard/PKPAuthorDashboardHandler.php @@ -261,12 +261,12 @@ public function setupTemplate($request) // Get an array of publications $publications = $submission->getData('publications'); /** @var Enumerable $publications */ - $publicationList = $publications->map(function ($publication) { + $publicationList = $publications->map(function ($publication, $submissionContext) { return [ 'id' => $publication->getId(), 'datePublished' => $publication->getData('datePublished'), 'status' => $publication->getData('status'), - 'version' => $publication->getData('version') + 'version' => Repo::publication()->getVersionDataDisplay($publication, null, $submissionContext), ]; })->values(); diff --git a/pages/workflow/PKPWorkflowHandler.php b/pages/workflow/PKPWorkflowHandler.php index 61efd0f96b3..5c9fee1657b 100644 --- a/pages/workflow/PKPWorkflowHandler.php +++ b/pages/workflow/PKPWorkflowHandler.php @@ -324,12 +324,12 @@ public function index($args, $request) // Get an array of publications $publications = $submission->getData('publications'); /** @var Enumerable $publications */ - $publicationList = $publications->map(function ($publication) { + $publicationList = $publications->map(function ($publication, $submissionContext) { return [ 'id' => $publication->getId(), 'datePublished' => $publication->getData('datePublished'), 'status' => $publication->getData('status'), - 'version' => $publication->getData('version') + 'version' => Repo::publication()->getVersionDataDisplay($publication, null, $submissionContext), ]; })->values(); diff --git a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php index db84fff1e15..cce15b3ba1c 100644 --- a/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php +++ b/plugins/importexport/native/filter/NativeXmlPKPPublicationFilter.php @@ -80,7 +80,9 @@ public function handleElement($node) $publication->stampModified(); $publication = $this->populateObject($publication, $node); - $publication->setData('version', $node->getAttribute('version')); + $publication->setData('versionStage', $node->getAttribute('version_stage')); + $publication->setData('versionMinor', $node->getAttribute('version_minor')); + $publication->setData('versionMajor', $node->getAttribute('version_major')); $publication->setData('seq', $node->getAttribute('seq')); $publication->setData('accessStatus', $node->getAttribute('access_status')); $publication->setData('status', $node->getAttribute('status')); diff --git a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php index 7c0bfd346b9..0c2e9aac428 100644 --- a/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php +++ b/plugins/importexport/native/filter/PKPPublicationNativeXmlFilter.php @@ -90,7 +90,10 @@ public function createEntityNode($doc, $entity) $this->addIdentifiers($doc, $entityNode, $entity); - $entityNode->setAttribute('version', $entity->getData('version') ?: 1); + $entityNode->setAttribute('version_stage', $entity->getData('versionStage')); + $entityNode->setAttribute('version_minor', $entity->getData('versionMinor')); + $entityNode->setAttribute('version_major', $entity->getData('versionMajor')); + $entityNode->setAttribute('status', $entity->getData('status')); if ($primaryContactId = $entity->getData('primaryContactId')) { $entityNode->setAttribute('primary_contact_id', $primaryContactId); diff --git a/plugins/importexport/native/pkp-native.xsd b/plugins/importexport/native/pkp-native.xsd index bdbf9a83b12..0473a40009f 100644 --- a/plugins/importexport/native/pkp-native.xsd +++ b/plugins/importexport/native/pkp-native.xsd @@ -267,10 +267,12 @@ - + + +