diff --git a/api/v1/navigations/PKPNavigationController.php b/api/v1/navigations/PKPNavigationController.php new file mode 100644 index 00000000000..385bb0a716e --- /dev/null +++ b/api/v1/navigations/PKPNavigationController.php @@ -0,0 +1,84 @@ +getPublic(...)) + ->name('navigation.get') + ->where('areaName', '[0-9]+|[a-zA-Z0-9\-_]+'); + } + + /** + * Get navigation menu by ID with formatted menu items and nesting + */ + public function getPublic(Request $illuminateRequest): JsonResponse + { + $navigationMenu = NavigationMenu::where('area_name', $illuminateRequest->route('areaName')) + ->where('context_id', $this->getRequest()->getContext()->getId())->first(); + + if (!$navigationMenu) { + return response()->json([ + 'error' => 'Navigation menu not found' + ], Response::HTTP_NOT_FOUND); + } + + return response()->json( + (new NavigationResource($navigationMenu))->toArray($illuminateRequest), + Response::HTTP_OK + ); + } + +} diff --git a/api/v1/navigations/resources/NavigationMenuItemResource.php b/api/v1/navigations/resources/NavigationMenuItemResource.php new file mode 100644 index 00000000000..43eb4d23878 --- /dev/null +++ b/api/v1/navigations/resources/NavigationMenuItemResource.php @@ -0,0 +1,53 @@ +getRequest()->getContext(); + $titleData = []; + if ($this->titleLocaleKey) { + foreach ($context->getSupportedLocales() as $locale) { + $titleData[$locale] = Locale::get($this->titleLocaleKey, [], $locale); + } + } + + return [ + 'id' => $this->id, + 'path' => $this->path, + 'type' => $this->type, + 'title' => $titleData + ]; + } + + /** + * @inheritDoc + */ + protected static function requiredKeys(): array + { + return []; + } +} diff --git a/api/v1/navigations/resources/NavigationResource.php b/api/v1/navigations/resources/NavigationResource.php new file mode 100644 index 00000000000..e0cee1a628d --- /dev/null +++ b/api/v1/navigations/resources/NavigationResource.php @@ -0,0 +1,45 @@ + $this->id, + 'title' => $this->title, + 'area_name' => $this->area_name, + 'context_id' => $this->context_id, + 'items' => NavigationMenuItemResource::collection($this->menuItems ?? collect(), $this->data ?? []), + ]; + } + + /** + * @inheritDoc + */ + protected static function requiredKeys(): array + { + return []; + } +} diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index edea72aeec9..52da476278e 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -36,6 +36,8 @@ use Illuminate\Support\LazyCollection; use Illuminate\Validation\Rule; use PKP\affiliation\Affiliation; +use PKP\API\v1\submissions\tasks\formRequests\AddTask; +use PKP\API\v1\submissions\tasks\resources\TaskResource; use PKP\components\forms\FormComponent; use PKP\components\forms\publication\PKPCitationsForm; use PKP\components\forms\publication\PKPMetadataForm; @@ -50,6 +52,7 @@ use PKP\core\PKPRequest; use PKP\db\DAORegistry; use PKP\decision\DecisionType; +use PKP\editorialTask\EditorialTask; use PKP\jobs\orcid\SendAuthorMail; use PKP\log\event\PKPSubmissionEventLogEntry; use PKP\mail\mailables\PublicationVersionNotify; @@ -60,7 +63,6 @@ use PKP\plugins\Hook; use PKP\plugins\PluginRegistry; use PKP\publication\helpers\PublicationVersionInfoResource; -use PKP\publication\PKPPublication; use PKP\security\authorization\ContextAccessPolicy; use PKP\security\authorization\DecisionWritePolicy; use PKP\security\authorization\internal\SubmissionCompletePolicy; @@ -121,6 +123,11 @@ class PKPSubmissionController extends PKPBaseController 'getChangeLanguageMetadata', 'changeVersion', 'getNextAvailableVersion', + 'addTask', + 'editTask', + 'deleteTask', + 'getTask', + 'getTasks', ]; /** @var array Handlers that must be authorized to write to a publication */ @@ -150,6 +157,12 @@ class PKPSubmissionController extends PKPBaseController Role::ROLE_ID_ASSISTANT ]; + public array $publicAccessRoutes = [ + 'getPublic', + 'getPublicationPublic', + 'getPublicationByVersionPublic' + ]; + /** * @copydoc \PKP\core\PKPBaseController::getHandlerPath() */ @@ -164,7 +177,6 @@ public function getHandlerPath(): string public function getRouteGroupMiddleware(): array { return [ - 'has.user', 'has.context', ]; } @@ -361,6 +373,49 @@ public function getGroupRoutes(): void ->middleware([ self::roleAuthorizer(Role::getAllRoles()), ]); + + Route::middleware([ + self::roleAuthorizer([ + Role::ROLE_ID_MANAGER, + Role::ROLE_ID_SUB_EDITOR, + Role::ROLE_ID_ASSISTANT, + Role::ROLE_ID_REVIEWER, + Role::ROLE_ID_AUTHOR, + ]), + ])->group(function () { + + Route::post('{submissionId}/tasks', $this->addTask(...)) + ->name('submission.task.add') + ->whereNumber('submissionId'); + + Route::put('{submissionId}/tasks/{taskId}', $this->editTask(...)) + ->name('submission.task.edit') + ->whereNumber(['submissionId', 'taskId']); + + Route::delete('{submissionId}/tasks/{taskId}', $this->deleteTask(...)) + ->name('submission.task.delete ') + ->whereNumber(['submissionId', 'taskId']); + + Route::get('{submissionId}/tasks/{taskId}', $this->getTask(...)) + ->name('submission.task.get') + ->whereNumber(['submissionId', 'taskId']); + + Route::get('{submissionId}/stage/{stageId}/tasks', $this->getTasks(...)) + ->name('submission.task.getMany') + ->whereNumber(['submissionId', 'stageId']); + }); + + Route::get('{submissionId}/public', $this->getPublic(...)) + ->name('submission.public/get') + ->where('submissionId', '[0-9]+|[a-zA-Z0-9\-_]+'); + + Route::get('{submissionId}/publications/{publicationId}/public', $this->getPublicationPublic(...)) + ->name('submission.publication.public/get') + ->whereNumber(['submissionId', 'publicationId']); + + Route::get('{submissionId}/version/{versionMajor}/{versionMinor}/public', $this->getPublicationByVersionPublic(...)) + ->name('submission.publication.version.public/get') + ->whereNumber(['submissionId', 'versionMajor', 'versionMinor']); } /** @@ -371,9 +426,10 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme $illuminateRequest = $args[0]; /** @var \Illuminate\Http\Request $illuminateRequest */ $actionName = static::getRouteActionName($illuminateRequest); - $this->addPolicy(new UserRolesRequiredPolicy($request), true); - - $this->addPolicy(new ContextAccessPolicy($request, $roleAssignments)); + if (!in_array($actionName, $this->publicAccessRoutes)) { + $this->addPolicy(new UserRolesRequiredPolicy($request), true); + $this->addPolicy(new ContextAccessPolicy($request, $roleAssignments)); + } if (in_array($actionName, $this->requiresSubmissionAccess)) { $this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments)); @@ -407,6 +463,24 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme $this->addPolicy(new PublicationAccessPolicy($request, $args, $roleAssignments)); } + // For operations to retrieve task(s), we just ensure that the user has access to it + if (in_array($actionName, ['getTask'])) { + $stageId = $request->getUserVar('stageId'); + $this->addPolicy(new QueryAccessPolicy($request, $args, $roleAssignments, !empty($stageId) ? (int) $stageId : null, 'taskId')); + } + + // To modify a task, need to check read and write access policies + if (in_array($actionName, ['editTask', 'deleteTask'])) { + $stageId = $request->getUserVar('stageId'); + $this->addPolicy(new QueryAccessPolicy($request, $args, $roleAssignments, !empty($stageId) ? (int) $stageId : null, 'taskId')); + $this->addPolicy(new QueryWritePolicy($request)); + } + + // To create a task or get a list of tasks, check if the user has access to the workflow stage; note that controller must ensure to get a list of tasks where user is a participant + if (in_array($actionName, ['addTask', 'getTasks'])) { + $this->addPolicy(new QueryWorkflowStageAccessPolicy($request, $args, $roleAssignments, (int) $request->getUserVar('stageId'))); + } + return parent::authorize($request, $args, $roleAssignments); } @@ -447,7 +521,7 @@ public function getMany(Request $illuminateRequest): JsonResponse /** @var \PKP\submission\GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($context->getId())->toAssociativeArray(); + $genres = $genreDao->getByContextId($context->getId())->toArray(); return response()->json([ 'itemsMax' => $collector->getCount(), @@ -541,6 +615,71 @@ protected function getSubmissionCollector(array $queryParams): Collector return $collector; } + public function getPublic(Request $illuminateRequest): JsonResponse + { + $request = $this->getRequest(); + $urlPath = $illuminateRequest->route('submissionId'); + + $submission = ctype_digit((string) $urlPath) + ? Repo::submission()->get((int) $urlPath, $request->getContext()->getId()) + : Repo::submission()->getByUrlPath($urlPath, $request->getContext()->getId()); + + $mappedSubmission = Repo::submission()->getSchemaMap()->mapPublic($submission); + + return response()->json($mappedSubmission, Response::HTTP_OK); + } + + public function getPublicationPublic(Request $illuminateRequest): JsonResponse + { + $submissionId = (int) $illuminateRequest->route('submissionId'); + $submission = Repo::submission()->get($submissionId); + $publication = Repo::publication()->get((int) $illuminateRequest->route('publicationId')); + + if (!$publication) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + if ($submission->getId() !== $publication->getData('submissionId')) { + return response()->json([ + 'error' => __('api.publications.403.submissionsDidNotMatch'), + ], Response::HTTP_FORBIDDEN); + } + + return response()->json( + Repo::publication()->getSchemaMap($submission, collect(), [])->map( + $publication, + isPublic: true, + ), + Response::HTTP_OK + ); + } + + public function getPublicationByVersionPublic(Request $illuminateRequest): JsonResponse + { + $submissionId = (int) $illuminateRequest->route('submissionId'); + $versionMajor = (int) $illuminateRequest->route('versionMajor'); + $versionMinor = (int) $illuminateRequest->route('versionMinor'); + $submission = Repo::submission()->get($submissionId); + $publication = Repo::publication()->getByVersion($submission, $versionMajor, $versionMinor); + + if (!$publication) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + return response()->json( + Repo::publication()->getSchemaMap($submission, collect(), [])->map( + $publication, + isPublic: true, + ), + Response::HTTP_OK + ); + + } + /** * Get a single submission */ @@ -558,7 +697,7 @@ public function get(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json(Repo::submission()->getSchemaMap()->map( $submission, @@ -693,7 +832,7 @@ public function add(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); if (!$userGroups instanceof LazyCollection) { $userGroups = $userGroups->lazy(); @@ -736,7 +875,7 @@ public function edit(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES); return response()->json(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres, $userRoles), Response::HTTP_OK); @@ -789,7 +928,7 @@ public function saveForLater(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES); return response()->json(Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres, $userRoles), Response::HTTP_OK); @@ -870,7 +1009,7 @@ public function submit(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES); $notificationManager = new NotificationManager(); @@ -903,7 +1042,7 @@ public function delete(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); $userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES); $submissionProps = Repo::submission()->getSchemaMap()->map($submission, $userGroups, $genres, $userRoles); @@ -938,7 +1077,7 @@ public function changeLocale(Request $illuminateRequest): JsonResponse $newLocale = $paramsSubmission['locale'] ?? null; // Submission language can not be changed when there are more than one publication or a publication's status is published - if (!$newLocale || count($submission->getData('publications')) > 1 || $publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if (!$newLocale || count($submission->getData('publications')) > 1 || $publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json(['error' => __('api.submission.403.cantChangeSubmissionLanguage')], Response::HTTP_FORBIDDEN); } @@ -1129,7 +1268,7 @@ public function getPublications(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json([ 'itemsMax' => $collector->getCount(), @@ -1162,7 +1301,7 @@ public function getPublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json( Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), @@ -1201,7 +1340,7 @@ public function addPublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json( Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), @@ -1259,13 +1398,13 @@ public function versionPublication(Request $illuminateRequest): JsonResponse // Check if user is subscribed to this type of notification emails if (!$notification || in_array( - Notification::NOTIFICATION_TYPE_SUBMISSION_NEW_VERSION, - $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings( - NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY, - $user->getId(), - (int) $context->getId() - ) - )) { + Notification::NOTIFICATION_TYPE_SUBMISSION_NEW_VERSION, + $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings( + NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY, + $user->getId(), + (int) $context->getId() + ) + )) { continue; } @@ -1285,7 +1424,7 @@ public function versionPublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json( Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), @@ -1316,7 +1455,7 @@ public function editPublication(Request $illuminateRequest): JsonResponse } // Publications can not be edited when they are published - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.cantEditPublished'), ], Response::HTTP_FORBIDDEN); @@ -1333,6 +1472,8 @@ public function editPublication(Request $illuminateRequest): JsonResponse $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_PUBLICATION, $illuminateRequest->input()); $params['id'] = $publication->getId(); + // Don't allow the status to be modified through the API. The `/publish` and /unpublish endpoints + // should be used instead. if (array_key_exists('status', $params) && is_null($params['status'])) { unset($params['status']); } @@ -1379,7 +1520,7 @@ public function editPublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json( Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), @@ -1412,7 +1553,7 @@ public function publishPublication(Request $illuminateRequest): JsonResponse ], Response::HTTP_FORBIDDEN); } - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.alreadyPublished'), ], Response::HTTP_FORBIDDEN); @@ -1440,7 +1581,7 @@ public function publishPublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json( Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), @@ -1468,7 +1609,7 @@ public function unpublishPublication(Request $illuminateRequest): JsonResponse ], Response::HTTP_FORBIDDEN); } - if (!in_array($publication->getData('status'), [PKPPublication::STATUS_PUBLISHED, PKPPublication::STATUS_SCHEDULED])) { + if (!in_array($publication->getData('status'), [PKPSubmission::STATUS_PUBLISHED, PKPSubmission::STATUS_SCHEDULED])) { return response()->json([ 'error' => __('api.publication.403.alreadyUnpublished'), ], Response::HTTP_FORBIDDEN); @@ -1483,7 +1624,7 @@ public function unpublishPublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); return response()->json( Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), @@ -1515,7 +1656,7 @@ public function deletePublication(Request $illuminateRequest): JsonResponse ], Response::HTTP_FORBIDDEN); } - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.cantDeletePublished'), ], Response::HTTP_FORBIDDEN); @@ -1525,7 +1666,7 @@ public function deletePublication(Request $illuminateRequest): JsonResponse /** @var GenreDAO $genreDao */ $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toAssociativeArray(); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); $output = Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication); @@ -1569,7 +1710,7 @@ public function getContributor(Request $illuminateRequest): JsonResponse } return response()->json( - Repo::author()->getSchemaMap($submission)->map($author), + Repo::author()->getSchemaMap()->map($author), Response::HTTP_OK ); } @@ -1600,7 +1741,7 @@ public function getContributors(Request $illuminateRequest): JsonResponse return response()->json([ 'itemsMax' => $collector->getCount(), - 'items' => Repo::author()->getSchemaMap($submission)->summarizeMany($authors)->values(), + 'items' => Repo::author()->getSchemaMap()->summarizeMany($authors)->values(), ], Response::HTTP_OK); } @@ -1630,7 +1771,7 @@ public function addContributor(Request $illuminateRequest): JsonResponse } // Publications can not be edited when they are published - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.cantEditPublished'), ], Response::HTTP_FORBIDDEN); @@ -1700,7 +1841,7 @@ public function addContributor(Request $illuminateRequest): JsonResponse } return response()->json( - Repo::author()->getSchemaMap($submission)->map($author), + Repo::author()->getSchemaMap()->map($author), Response::HTTP_OK ); } @@ -1724,7 +1865,7 @@ public function deleteContributor(Request $illuminateRequest): JsonResponse } // Publications can not be edited when they are published - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.cantEditPublished'), ], Response::HTTP_FORBIDDEN); @@ -1748,7 +1889,7 @@ public function deleteContributor(Request $illuminateRequest): JsonResponse ], Response::HTTP_NOT_FOUND); } - $output = Repo::author()->getSchemaMap($submission)->map($author); + $output = Repo::author()->getSchemaMap()->map($author); Repo::author()->delete($author); @@ -1786,7 +1927,7 @@ public function editContributor(Request $illuminateRequest): JsonResponse } // Publications can not be edited when they are published - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.cantEditPublished'), ], Response::HTTP_FORBIDDEN); @@ -1848,7 +1989,7 @@ public function editContributor(Request $illuminateRequest): JsonResponse $author = Repo::author()->get($author->getId()); return response()->json( - Repo::author()->getSchemaMap($submission)->map($author), + Repo::author()->getSchemaMap()->map($author), Response::HTTP_OK ); } @@ -1879,7 +2020,7 @@ public function saveContributorsOrder(Request $illuminateRequest): JsonResponse } // Publications can not be edited when they are published - if ($publication->getData('status') === PKPPublication::STATUS_PUBLISHED) { + if ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) { return response()->json([ 'error' => __('api.publication.403.cantEditPublished'), ], Response::HTTP_FORBIDDEN); @@ -1901,7 +2042,7 @@ public function saveContributorsOrder(Request $illuminateRequest): JsonResponse ->filterByPublicationIds([$publication->getId()]) ->getMany(); - $authorsArray = Repo::author()->getSchemaMap($submission)->summarizeMany($authors)->toArray(); + $authorsArray = Repo::author()->getSchemaMap()->summarizeMany($authors)->toArray(); $indexedArray = array_values($authorsArray); return response()->json( @@ -1952,16 +2093,138 @@ public function addDecision(Request $illuminateRequest): JsonResponse return response()->json(Repo::decision()->getSchemaMap()->map($decision), Response::HTTP_OK); } + /** + * Creates a task or discussion associated with the submission + */ + public function addTask(AddTask $illuminateRequest): JsonResponse + { + $validated = $illuminateRequest->validated(); + + $editorialTask = new EditorialTask($validated); + $editorialTask->save(); + + return response()->json( + (new TaskResource($editorialTask->refresh())) + ->toArray($illuminateRequest), + Response::HTTP_OK + ); + } + + /** + * Get a single task or discussion associated with the submission + */ + public function getTask(Request $illuminateRequest): JsonResponse + { + $editTask = EditorialTask::find($illuminateRequest->route('taskId')); + + if (!$editTask) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + return response()->json( + (new TaskResource($editTask))->toArray($illuminateRequest), + Response::HTTP_OK + ); + } + + /** + * Get a list of all available tasks and discussions related to the submission + */ + public function getTasks(Request $illuminateRequest): JsonResponse + { + $currentUser = $this->getRequest()->getUser(); + $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ + + // Managers have access to all tasks and discussions irrespectable of the participation + if ($currentUser->hasRole([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $submission->getData('contextId'))) { + $collector = EditorialTask::withAssocType(PKPApplication::ASSOC_TYPE_SUBMISSION) + ->withAssocIds([$submission->getId()]) + ->withStageId($illuminateRequest->route('stageId')); + } else { + // Other users have access to tasks, where they are included as participants + $collector = EditorialTask::withAssocType(PKPApplication::ASSOC_TYPE_SUBMISSION) + ->withAssocIds([$submission->getId()]) + ->withParticipantIds([$currentUser->getId()]) + ->withStageId($illuminateRequest->route('stageId')); + } + + foreach ($illuminateRequest->query() as $param => $val) { + switch ($param) { + case 'isOpen': + $collector = $collector->withClosed((bool) $val); + break; + } + } + + $tasks = $collector->get(); + + return response()->json([ + 'items' => TaskResource::collection($tasks), + 'itemMax' => $tasks->count(), + ], Response::HTTP_OK); + } + + /** + * Edit a task or discussion associated with the submission + */ + public function editTask(EditTask $illuminateRequest): JsonResponse + { + $editTask = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_QUERY); + + if (!$editTask) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + $validated = $illuminateRequest->validated(); + + if (!$editTask->update($validated)) { + return response()->json([ + 'error' => __('api.409.resourceActionConflict'), + ], Response::HTTP_CONFLICT); + } + + return response()->json( + (new TaskResource($editTask->refresh())) + ->toArray($illuminateRequest), + Response::HTTP_OK + ); + } + + /** + * Remove task or discussion associated with the submission + */ + public function deleteTask(Request $illuminateRequest): JsonResponse + { + $editTask = EditorialTask::find($illuminateRequest->route('taskId')); + + if (!$editTask) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + $editTask->delete(); + + return response()->json([], Response::HTTP_OK); + } + + /** +======= +>>>>>>> main /** * Is the current user an editor */ protected function isEditor(): bool { return !empty( - array_intersect( - Section::getEditorRestrictedRoles(), - $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES) - ) + array_intersect( + Section::getEditorRestrictedRoles(), + $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES) + ) ); } @@ -2115,7 +2378,7 @@ protected function getPublicationIdentifierForm(Request $illuminateRequest): Jso /** * Get Publication TitleAbstract Form component - */ + */ protected function getPublicationTitleAbstractForm(Request $illuminateRequest): JsonResponse { $data = $this->getSubmissionAndPublicationData($illuminateRequest); @@ -2164,7 +2427,7 @@ protected function getChangeLanguageMetadata(Request $illuminateRequest): JsonRe /** * Utility method used to get the metadata locale information for a submission publications and context - */ + */ protected function getPublicationFormLocales(Context $context, Submission $submission): array { return collect($context->getSupportedSubmissionMetadataLocaleNames() + $submission->getPublicationLanguageNames()) @@ -2314,7 +2577,7 @@ protected function copyMultilingualData(Submission $submission, string $newLocal }); } - protected function validateVersionStage(Request $illuminateRequest): VersionStage + private function validateVersionStage(Request $illuminateRequest): VersionStage { $validator = Validator::make($illuminateRequest->all(), [ 'versionStage' => [ @@ -2330,7 +2593,7 @@ protected function validateVersionStage(Request $illuminateRequest): VersionStag return VersionStage::from($validator->validated()['versionStage']); } - protected function validateVersionIsMinor(Request $illuminateRequest): ?bool + private function validateVersionIsMinor(Request $illuminateRequest): ?bool { return filter_var( $illuminateRequest->input('versionIsMinor') ?? true, diff --git a/classes/core/maps/Schema.php b/classes/core/maps/Schema.php index 3e312d53b9d..ea4db4a38e8 100644 --- a/classes/core/maps/Schema.php +++ b/classes/core/maps/Schema.php @@ -62,17 +62,17 @@ public function __construct(PKPRequest $request, ?Context $context, PKPSchemaSer /** * Get the property names of this entity according to its schema */ - protected function getProps(): array + protected function getProps(bool $isPublic = false): array { - return $this->props ?? $this->schemaService->getFullProps($this->schema); + return $this->props ?? $this->schemaService->getFullProps($this->schema, $isPublic); } /** * Get the property names for a summary of this entity according to its schema */ - protected function getSummaryProps(): array + protected function getSummaryProps(bool $isPublic = false): array { - return $this->summaryProps ?? $this->schemaService->getSummaryProps($this->schema); + return $this->summaryProps ?? $this->schemaService->getSummaryProps($this->schema, $isPublic); } /** diff --git a/classes/navigationMenu/models/NavigationMenu.php b/classes/navigationMenu/models/NavigationMenu.php new file mode 100644 index 00000000000..921ef4e2d78 --- /dev/null +++ b/classes/navigationMenu/models/NavigationMenu.php @@ -0,0 +1,73 @@ + 'int', + 'context_id' => 'int', + 'area_name' => 'string', + 'title' => 'string', + ]; + + /** + * Accessor and Mutator for primary key => id + */ + protected function id(): Attribute + { + return Attribute::make( + get: fn ($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn ($value) => [$this->primaryKey => $value], + ); + } + + /** + * Navigation menu item assignments for this menu + */ + public function assignments(): HasMany + { + return $this->hasMany(\PKP\navigationMenu\models\NavigationMenuItemAssignment::class, 'navigation_menu_id'); + } + + /** + * Navigation menu items assigned to this menu (through assignments) + */ + public function menuItems(): BelongsToMany + { + return $this->belongsToMany( + NavigationMenuItem::class, + 'navigation_menu_item_assignments', + 'navigation_menu_id', + 'navigation_menu_item_id' + )->withPivot(['parent_id', 'seq']); + } + +} diff --git a/classes/navigationMenu/models/NavigationMenuItem.php b/classes/navigationMenu/models/NavigationMenuItem.php new file mode 100644 index 00000000000..0e73f8dc513 --- /dev/null +++ b/classes/navigationMenu/models/NavigationMenuItem.php @@ -0,0 +1,137 @@ + 'int', + 'context_id' => 'int', + 'path' => 'string', + 'type' => 'string', + ]; + + /** + * @inheritDoc + */ + public function getSettingsTable(): string + { + return 'navigation_menu_item_settings'; + } + + /** + * @inheritDoc + */ + public static function getSchemaName(): ?string + { + return null; + } + + /** + * @inheritDoc + */ + public function getSettings(): array + { + return [ + 'title', + 'titleLocaleKey', + 'sequence', + 'remoteUrl', + ]; + } + + /** + * @inheritDoc + */ + public function getMultilingualProps(): array + { + return [ + 'title', + ]; + } + + /** + * Accessor and Mutator for primary key => id + */ + protected function id(): Attribute + { + return Attribute::make( + get: fn ($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn ($value) => [$this->primaryKey => $value], + ); + } + + /** + * Navigation menu item assignments for this item + */ + public function assignments(): HasMany + { + return $this->hasMany(NavigationMenuItemAssignment::class, 'navigation_menu_item_id'); + } + + /** + * Navigation menus this item is assigned to (through assignments) + */ + public function navigationMenus(): BelongsToMany + { + return $this->belongsToMany( + NavigationMenu::class, + 'navigation_menu_item_assignments', + 'navigation_menu_item_id', + 'navigation_menu_id' + )->withPivot(['parent_id', 'seq']); + } + + /** + * Settings for this navigation menu item + */ + public function settings(): HasMany + { + return $this->hasMany(NavigationMenuItemSetting::class, 'navigation_menu_item_id'); + } + + /** + * Get localized title from settings + */ + public function getLocalizedTitle(?string $locale = null): string + { + $locale = $locale ?? app()->getLocale(); + $titleKey = $this->getSetting('titleLocaleKey', $locale); + if ($titleKey) { + return __($titleKey, [], $locale); + } + + return $this->getSetting('title', $locale) ?? ''; + } + +} diff --git a/classes/navigationMenu/models/NavigationMenuItemAssignment.php b/classes/navigationMenu/models/NavigationMenuItemAssignment.php new file mode 100644 index 00000000000..84f2df97f18 --- /dev/null +++ b/classes/navigationMenu/models/NavigationMenuItemAssignment.php @@ -0,0 +1,85 @@ + 'int', + 'navigation_menu_id' => 'int', + 'navigation_menu_item_id' => 'int', + 'parent_id' => 'int', + 'seq' => 'int', + ]; + + /** + * Accessor and Mutator for primary key => id + */ + protected function id(): Attribute + { + return Attribute::make( + get: fn ($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn ($value) => [$this->primaryKey => $value], + ); + } + + /** + * Navigation menu this assignment belongs to + */ + public function navigationMenu(): BelongsTo + { + return $this->belongsTo(NavigationMenu::class, 'navigation_menu_id'); + } + + /** + * Navigation menu item this assignment refers to + */ + public function navigationMenuItem(): BelongsTo + { + return $this->belongsTo(NavigationMenuItem::class, 'navigation_menu_item_id'); + } + + /** + * Parent assignment (for nested menu items) + */ + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id', 'navigation_menu_item_assignment_id'); + } + + /** + * Child assignments (for nested menu items) + */ + public function children() + { + return $this->hasMany(self::class, 'parent_id', 'navigation_menu_item_assignment_id'); + } + +} diff --git a/classes/navigationMenu/models/NavigationMenuItemSetting.php b/classes/navigationMenu/models/NavigationMenuItemSetting.php new file mode 100644 index 00000000000..d5c6810a340 --- /dev/null +++ b/classes/navigationMenu/models/NavigationMenuItemSetting.php @@ -0,0 +1,63 @@ + 'int', + 'navigation_menu_item_id' => 'int', + 'locale' => 'string', + 'setting_name' => 'string', + 'setting_value' => 'string', + 'setting_type' => 'string', + ]; + + /** + * Accessor and Mutator for primary key => id + */ + protected function id(): Attribute + { + return Attribute::make( + get: fn ($value, $attributes) => $attributes[$this->primaryKey] ?? null, + set: fn ($value) => [$this->primaryKey => $value], + ); + } + + /** + * Navigation menu item this setting belongs to + */ + public function navigationMenuItem(): BelongsTo + { + return $this->belongsTo(NavigationMenuItem::class, 'navigation_menu_item_id'); + } + +} diff --git a/classes/publication/Repository.php b/classes/publication/Repository.php index bb030b5e15d..259b9118208 100644 --- a/classes/publication/Repository.php +++ b/classes/publication/Repository.php @@ -89,6 +89,29 @@ public function get(int $id, ?int $submissionId = null): ?Publication return $this->dao->get($id, $submissionId); } + /** + * Get a publication by version for a submission + * + * @param Submission $submission The submission to search within + * @param int $versionMajor The major version number + * @param int $versionMinor The minor version number + * + * @return Publication|null The publication if found, null otherwise + */ + public function getByVersion(Submission $submission, int $versionMajor, int $versionMinor): ?Publication + { + return $this->getCollector() + ->filterBySubmissionIds([$submission->getId()]) + ->getMany() + ->filter(function (Publication $publication) use ($versionMajor, $versionMinor) { + $version = $publication->getVersion(); + return $version && + $version->majorNumbering === $versionMajor && + $version->minorNumbering === $versionMinor; + }) + ->first(); + } + /** @copydoc DAO::getCollector() */ public function getCollector(): Collector { diff --git a/classes/publication/maps/Schema.php b/classes/publication/maps/Schema.php index 91b21eafb8b..31d2eef2b2d 100644 --- a/classes/publication/maps/Schema.php +++ b/classes/publication/maps/Schema.php @@ -58,9 +58,9 @@ public function __construct(Submission $submission, Enumerable $userGroups, arra * * Includes all properties in the publication schema. */ - public function map(Publication $item, bool $anonymize = false): array + public function map(Publication $item, bool $anonymize = false, bool $isPublic = false): array { - return $this->mapByProperties($this->getProps(), $item, $anonymize); + return $this->mapByProperties($this->getProps($isPublic), $item, $anonymize); } /** @@ -68,9 +68,9 @@ public function map(Publication $item, bool $anonymize = false): array * * Includes properties with the apiSummary flag in the publication schema. */ - public function summarize(Publication $item, bool $anonymize = false): array + public function summarize(Publication $item, bool $anonymize = false, bool $isPublic = false): array { - return $this->mapByProperties($this->getSummaryProps(), $item, $anonymize); + return $this->mapByProperties($this->getSummaryProps($isPublic), $item, $anonymize); } /** diff --git a/classes/services/PKPSchemaService.php b/classes/services/PKPSchemaService.php index ad24466a757..55f1ba0e915 100644 --- a/classes/services/PKPSchemaService.php +++ b/classes/services/PKPSchemaService.php @@ -165,13 +165,21 @@ public function merge($baseSchema, $additionalSchema) * * @return array List of property names */ - public function getSummaryProps($schemaName) + public function getSummaryProps($schemaName, $shouldBePublic = false) { $schema = $this->get($schemaName); $props = []; foreach ($schema->properties as $propName => $propSchema) { if (!empty($propSchema->apiSummary) && empty($propSchema->writeOnly)) { - $props[] = $propName; + if ($shouldBePublic) { + $canDisplay = !empty($propSchema->isPublic); + } else { + $canDisplay = true; + } + + if ($canDisplay) { + $props[] = $propName; + } } } @@ -188,14 +196,23 @@ public function getSummaryProps($schemaName) * * @return array List of property names */ - public function getFullProps($schemaName) + public function getFullProps($schemaName, $shouldBePublic = false) { $schema = $this->get($schemaName); $propNames = []; foreach ($schema->properties as $propName => $propSchema) { if (empty($propSchema->writeOnly)) { - $propNames[] = $propName; + if ($shouldBePublic) { + $canDisplay = !empty($propSchema->isPublic); + } else { + $canDisplay = true; + } + + if ($canDisplay) { + $propNames[] = $propName; + } + } } diff --git a/classes/submission/maps/Schema.php b/classes/submission/maps/Schema.php index 52899e1ea83..9952611c78b 100644 --- a/classes/submission/maps/Schema.php +++ b/classes/submission/maps/Schema.php @@ -59,19 +59,19 @@ class Schema extends \PKP\core\maps\Schema public array $genres; /** @var Enumerable Review assignments associated with submissions. */ - public ?Enumerable $reviewAssignments = null; + public Enumerable $reviewAssignments; /** @var Enumerable Stage assignments associated with submissions. */ - public ?Enumerable $stageAssignments = null; + public Enumerable $stageAssignments; /** @var Enumerable Decisions associated with submissions. */ - public ?Enumerable $decisions = null; + public Enumerable $decisions; /** @var Enumerable Reviewer Suggestions associated with submissions. */ - public ?Enumerable $reviewerSuggestions = null; + public Enumerable $reviewerSuggestions; /** Workflow stage files associated with submissions. */ - public ?Enumerable $submissionStageFiles = null; + public Enumerable $submissionStageFiles; /** * Get extra property names used in the submissions list * @@ -117,6 +117,20 @@ protected function getSubmissionsListProps(): array return $props; } + /** + * Map a submission for the public API + * + */ + public function mapPublic(Submission $item): array + { + $this->userGroups = collect(); + $this->genres = []; + $props = $this->getProps(true); + unset($props['publications']); + + return $this->mapByProperties($this->getProps(true), $item); + } + /** * Map a submission * @@ -141,7 +155,8 @@ public function map( ?Enumerable $decisions = null, bool|Collection $anonymizeReviews = false, ?Enumerable $reviewerSuggestions = null, - ?Enumerable $stageFiles = null + ?Enumerable $stageFiles = null, + bool $isPublic = false, ): array { $this->userGroups = $userGroups; $this->genres = $genres; @@ -153,7 +168,7 @@ public function map( $this->submissionStageFiles = $stageFiles ?? $this->getStageFilesBySubmissions(collect([$item]), [SubmissionFile::SUBMISSION_FILE_COPYEDIT]); $this->addAppSpecificData(collect([$item])); - return $this->mapByProperties($this->getProps(), $item, $anonymizeReviews); + return $this->mapByProperties($this->getProps($isPublic), $item, $anonymizeReviews); } /** @@ -219,9 +234,9 @@ public function mapMany( $this->addAppSpecificData($collection); $associatedReviewAssignments = $this->reviewAssignments->groupBy(fn (ReviewAssignment $reviewAssignment, int $key) => - $reviewAssignment->getData('submissionId')); + $reviewAssignment->getData('submissionId')); $associatedStageAssignments = $this->stageAssignments->groupBy(fn (StageAssignment $stageAssignment, int $key) => - $stageAssignment->submissionId); + $stageAssignment->submissionId); $associatedDecisions = $this->decisions->groupBy( fn (Decision $decision, int $key) => $decision->getData('submissionId') @@ -315,7 +330,6 @@ public function summarizeMany(Enumerable $collection, Enumerable $userGroups, ar * @param ?Enumerable $reviewAssignments review assignments associated with a submission * @param ?Enumerable $stageAssignments stage assignments associated with a submission * @param bool|Collection $anonymizeReviews List of review assignment IDs to anonymize - * @param ?Enumerable $reviewerSuggestions List of stage files associated with a submission * @param ?Enumerable $stageFiles List of stage files associated with a submission */ public function mapToSubmissionsList( @@ -326,16 +340,15 @@ public function mapToSubmissionsList( ?Enumerable $stageAssignments = null, ?Enumerable $decisions = null, bool|Collection $anonymizeReviews = false, - ?Enumerable $reviewerSuggestions = null, ?Enumerable $stageFiles = null ): array { $this->userGroups = $userGroups; $this->genres = $genres; - $this->reviewAssignments = $reviewAssignments; - $this->stageAssignments = $stageAssignments; - $this->decisions = $decisions; - $this->reviewerSuggestions = $reviewerSuggestions; - $this->submissionStageFiles = $stageFiles; + $this->reviewAssignments = $reviewAssignments ?? Repo::reviewAssignment()->getCollector()->filterBySubmissionIds([$item->getId()])->getMany()->remember(); + $this->stageAssignments = $stageAssignments ?? $this->getStageAssignmentsBySubmissions(collect([$item])); + $this->decisions = $decisions ?? Repo::decision()->getCollector()->filterBySubmissionIds([$item->getId()])->getMany()->remember(); + $this->reviewerSuggestions = $reviewerSuggestions ?? ReviewerSuggestion::withSubmissionIds($item->getId())->get(); + $this->submissionStageFiles = $stageFiles ?? $this->getStageFilesBySubmissions(collect([$item]), [SubmissionFile::SUBMISSION_FILE_COPYEDIT]); $this->addAppSpecificData(collect([$item])); return $this->mapByProperties($this->getSubmissionsListProps(), $item, $anonymizeReviews); @@ -404,7 +417,6 @@ public function mapManyToSubmissionsList( $associatedStageAssignments->get($item->getId()), $associatedDecisions->get($item->getId()), $anonymizeReviews, - $associatedReviewerSuggestions->get($item->getId()), $associatedSubmissionStageFiles->get($item->getId()) ) ); @@ -459,7 +471,7 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co if (in_array('publications', $props)) { $currentUserReviewAssignment = Repo::reviewAssignment()->getCollector() ->filterBySubmissionIds([$submission->getId()]) - ->filterByReviewerIds([$this->request->getUser()->getId()], true) + ->filterByReviewerIds([$this->request->getUser()?->getId()], true) ->getMany() ->first(); $anonymize = $currentUserReviewAssignment && $currentUserReviewAssignment->getReviewMethod() === ReviewAssignment::SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS; @@ -468,7 +480,7 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co $reviewRounds = $this->getReviewRoundsFromSubmission($submission); $currentReviewRound = $reviewRounds->sortKeys()->last(); /** @var ReviewRound|null $currentReviewRound */ $stages = in_array('stages', $props) ? - $this->getPropertyStages($this->stageAssignments, $submission, $this->submissionStageFiles, $this->decisions ?? null, $currentReviewRound) : + $this->getPropertyStages($this->stageAssignments, $this->reviewAssignments, $submission, $this->submissionStageFiles, $this->decisions ?? null, $currentReviewRound) : []; foreach ($props as $prop) { @@ -491,10 +503,10 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co break; case 'canCurrentUserChangeMetadata': // Identify if current user can change metadata. Consider roles in the active stage. - $output[$prop] = $this->canChangeMetadata($this->stageAssignments); + $output[$prop] = $this->canChangeMetadata($this->stageAssignments, $submission); break; case 'editorAssigned': - $output[$prop] = $this->stageAssignments && $this->getPropertyStageAssignments($this->stageAssignments); + $output[$prop] = $this->getPropertyStageAssignments($this->stageAssignments); break; case 'metadataLocales': $output[$prop] = collect($this->context->getSupportedSubmissionMetadataLocaleNames() + $submission->getPublicationLanguageNames()) @@ -505,17 +517,20 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co $output[$prop] = Repo::publication()->getSchemaMap($submission, $this->userGroups, $this->genres) ->summarizeMany($submission->getData('publications'), $anonymize)->values(); break; + case 'currentPublication': + $output[$prop] = Repo::publication()->getSchemaMap($submission, $this->userGroups, $this->genres)->summarize($submission->getCurrentPublication()); + break; case 'recommendationsIn': - $output[$prop] = $currentReviewRound && $this->stageAssignments ? $this->areRecommendationsIn($currentReviewRound, $this->stageAssignments) : null; + $output[$prop] = $currentReviewRound ? $this->areRecommendationsIn($currentReviewRound, $this->stageAssignments) : null; break; case 'reviewAssignments': - $output[$prop] = $this->reviewAssignments ? $this->getPropertyReviewAssignments($this->reviewAssignments, $stages, $anonymizeReviews) : []; + $output[$prop] = $this->getPropertyReviewAssignments($this->reviewAssignments, $anonymizeReviews, $submission, $stages); break; case 'participants': $output[$prop] = $this->getPropertyParticipants($submission); break; case 'reviewersNotAssigned': - $output[$prop] = $currentReviewRound && $this->reviewAssignments?->count() < $this->context->getNumReviewsPerSubmission(); + $output[$prop] = $currentReviewRound && $this->reviewAssignments->count() < $this->context->getNumReviewsPerSubmission(); break; case 'reviewRounds': $output[$prop] = $this->getPropertyReviewRounds($reviewRounds); @@ -548,7 +563,7 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co $output[$prop] = Repo::submission()->getWorkflowUrlByUserRoles($submission); break; case 'reviewerSuggestions': - $output[$prop] = $this->reviewerSuggestions ? $this->getPropertyReviewerSuggestions($this->reviewerSuggestions) : []; + $output[$prop] = $this->getPropertyReviewerSuggestions($this->reviewerSuggestions); break; default: $output[$prop] = $submission->getData($prop); @@ -562,14 +577,14 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co /** * Determine whether current user is able to change metadata */ - protected function canChangeMetadata(?Enumerable $stageAssignments): bool + protected function canChangeMetadata(Enumerable $stageAssignments, Submission $submission): bool { $currentUser = Application::get()->getRequest()->getUser(); $isAssigned = false; $canChangeMetadata = false; // Check if stage assignment is associated with the current user and edit metadata flag - foreach ($stageAssignments ?? [] as $stageAssignment) { + foreach ($stageAssignments as $stageAssignment) { if ($stageAssignment->userId === $currentUser->getId()) { $isAssigned = true; if ($stageAssignment->canChangeMetadata) { @@ -583,7 +598,7 @@ protected function canChangeMetadata(?Enumerable $stageAssignments): bool return true; } - // If user is not assigned, check editorial global roles, journal admin and managers should have access for editing metadata + // If user is assigned, check editorial global roles, journal admin and managers should have access for editing metadata if (!$isAssigned) { if (!empty(array_intersect( $this->userRoles, @@ -633,13 +648,12 @@ public function summarizeReviewerSuggestion(Enumerable $reviewerSuggestions): ar /** * Get details about the review assignments for a submission */ - protected function getPropertyReviewAssignments(Enumerable $reviewAssignments, array $stages, bool|Collection $anonymizeReviews = false): array + protected function getPropertyReviewAssignments(Enumerable $reviewAssignments, bool|Collection $anonymizeReviews = false, Submission $submission, array $stages): array { $request = Application::get()->getRequest(); $currentUser = $request->getUser(); $reviews = []; - foreach ($reviewAssignments as $reviewAssignment) { /** @var \PKP\submission\reviewAssignment\ReviewAssignment $reviewAssignment */ // skip declined/cancelled assignments if the user lacks permission for this specific stage. if ( @@ -828,7 +842,7 @@ protected function getPropertyParticipants(Submission $submission): array * } * ] */ - protected function getPropertyStages(?Enumerable $stageAssignments, Submission $submission, ?Enumerable $stageFiles, ?Enumerable $decisions, ?ReviewRound $currentReviewRound): array + protected function getPropertyStages(Enumerable $stageAssignments, Enumerable $reviewAssignments, Submission $submission, Enumerable $stageFiles, ?Enumerable $decisions, ?ReviewRound $currentReviewRound): array { $request = Application::get()->getRequest(); $currentUser = $request->getUser(); @@ -848,7 +862,7 @@ protected function getPropertyStages(?Enumerable $stageAssignments, Submission $ 'currentUserAssignedRoles' => [], ]; - if ($stageId === WORKFLOW_STAGE_ID_EDITING && $stageFiles) { + if ($stageId === WORKFLOW_STAGE_ID_EDITING) { $stages[$stageId]['uploadedFilesCount'] = $stageFiles->filter(fn (SubmissionFile $file) => $file->getData('fileStage') == SubmissionFile::SUBMISSION_FILE_COPYEDIT)->count(); } else { // A `null` value is used to indicate that no count data is available. @@ -863,7 +877,7 @@ protected function getPropertyStages(?Enumerable $stageAssignments, Submission $ $isCurrentUserDecidingEditor = false; // Determine stage assignment related data - foreach ($stageAssignments ?? [] as $stageAssignment) { + foreach ($stageAssignments as $stageAssignment) { // Record recommendations for review stages if ($stageAssignment->recommendOnly) { @@ -928,7 +942,7 @@ protected function getPropertyStages(?Enumerable $stageAssignments, Submission $ } // if the current user is not assigned in any non-revoked role but has a global role as a manager or admin, consider it in the submission - if (!$isAssignedInAnyRole && $this->reviewAssignments) { + if (!$isAssignedInAnyRole) { $hasCurrentUserReviewAssignment = $this->reviewAssignments->contains( fn (ReviewAssignment $reviewAssignment) => $reviewAssignment->getReviewerId() === $currentUser->getId() && @@ -1039,9 +1053,9 @@ protected function getAssignmentRoles(StageAssignment $stageAssignment): ?int $userGroup = $stageAssignment->userGroup; $userUserGroup = $userGroup->userUserGroups->first( fn (UserUserGroup $userUserGroup) => - $userUserGroup->userId === $stageAssignment->userId && // Check if user is associated with stage assignment - (!$userUserGroup->dateEnd || $userUserGroup->dateEnd->gt(now())) && - (!$userUserGroup->dateStart || $userUserGroup->dateStart->lte(now())) + $userUserGroup->userId === $stageAssignment->userId && // Check if user is associated with stage assignment + (!$userUserGroup->dateEnd || $userUserGroup->dateEnd->gt(now())) && + (!$userUserGroup->dateStart || $userUserGroup->dateStart->lte(now())) ); return $userUserGroup ? $userGroup->roleId : null; @@ -1055,9 +1069,9 @@ protected function getAssignmentRoles(StageAssignment $stageAssignment): ?int protected function getPropertyStageAssignments(Enumerable $stageAssignments): bool { return $stageAssignments->isNotEmpty() && $stageAssignments->contains( - fn (StageAssignment $stageAssignment) => - !$stageAssignment->recommendOnly - ); + fn (StageAssignment $stageAssignment) => + !$stageAssignment->recommendOnly + ); } protected function getUserGroup(int $userGroupId): ?UserGroup @@ -1082,8 +1096,7 @@ protected function getStageAssignmentsBySubmissions(Enumerable $submissions, arr $stageAssignments = StageAssignment::with(['userGroup.userUserGroups', 'userGroup.userGroupStages']) ->withSubmissionIds($submissionIds) ->withRoleIds(empty($roleIds) ? null : $roleIds) - ->lazy() - ->remember(); + ->lazy(); return $stageAssignments; } @@ -1128,7 +1141,7 @@ protected function getAvailableEditorialDecisions(int $stageId, Submission $subm /** * Check if a user can make Decisions or Recommendations on a submission's stage - */ + */ protected function checkDecisionPermissions(int $stageId, Submission $submission, User $user, int $contextId): array { /** @var StageAssignment[] $editorsStageAssignments*/ @@ -1201,8 +1214,7 @@ protected function getStageFilesBySubmissions(Enumerable $submissions, array $st ->getCollector() ->filterBySubmissionIds($submissionIds) ->filterByFileStages($stageIds) - ->getMany() - ->remember(); + ->getMany(); } /** diff --git a/schemas/publication.json b/schemas/publication.json index 5dc1a9c5727..a72be4f2316 100644 --- a/schemas/publication.json +++ b/schemas/publication.json @@ -11,21 +11,24 @@ "type": "string", "format": "uri", "readOnly": true, - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "abstract": { "type": "string", "multilingual": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "plainLanguageSummary": { "type": "string", "multilingual": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "authors": { "type": "array", @@ -39,7 +42,8 @@ "type": "string", "description": "All of the authors rendered with the appropriate separators according to the locale.", "apiSummary": true, - "readOnly": true + "readOnly": true, + "isPublic": true }, "authorsStringIncludeInBrowse": { "type": "string", @@ -51,7 +55,8 @@ "type": "string", "description": "A shortened version of the authors rendered with the appropriate separators according to the locale.", "apiSummary": true, - "readOnly": true + "readOnly": true, + "isPublic": true }, "categoryIds": { "type": "array", @@ -67,7 +72,8 @@ "readOnly": true, "items": { "type": "string" - } + }, + "isPublic": true }, "citationsRaw": { "type": "string", @@ -75,7 +81,8 @@ "readOnly": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "copyrightHolder": { "type": "string", @@ -83,14 +90,16 @@ "multilingual": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "copyrightYear": { "type": "integer", "description": "The copyright year for this publication.", "validation": [ "nullable" - ] + ], + "isPublic": true }, "coverage": { "type": "string", @@ -98,7 +107,8 @@ "multilingual": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "coverImage": { "type": "object", @@ -124,7 +134,8 @@ "nullable" ] } - } + }, + "isPublic": true }, "createdAt": { "type": "string", @@ -132,7 +143,8 @@ "validation": [ "date_format:Y-m-d H:i:s" ], - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "dataAvailability": { "type": "string", @@ -148,7 +160,8 @@ "validation": [ "nullable", "date_format:Y-m-d" - ] + ], + "isPublic": true }, "disciplines": { "type": "array", @@ -176,14 +189,16 @@ ] } } - } + }, + "isPublic": true }, "doiObject": { "type": "object", "description": "An object representing the DOI for this publication.", "apiSummary": true, "readOnly": true, - "$ref": "#/definitions/Doi" + "$ref": "#/definitions/Doi", + "isPublic": true }, "doiId": { "type": "integer", @@ -198,12 +213,14 @@ "description": "The combined prefix, title and subtitle. This may include inline HTML tags, such as `` and ``.", "multilingual": true, "readOnly": true, - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "id": { "type": "integer", "readOnly": true, - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "keywords": { "type": "array", @@ -231,13 +248,15 @@ ] } } - } + }, + "isPublic": true }, "lastModified": { "type": "string", "validation": [ "date_format:Y-m-d H:i:s" - ] + ], + "isPublic": true }, "licenseUrl": { "type": "string", @@ -245,13 +264,15 @@ "validation": [ "nullable", "url" - ] + ], + "isPublic": true }, "locale": { "type": "string", "description": "The primary locale of the submission this publication is attached to. This locale is used as the fallback when a language is missing from a multilingual property.", "apiSummary": true, - "readOnly": true + "readOnly": true, + "isPublic": true }, "prefix": { "type": "string", @@ -260,7 +281,8 @@ "apiSummary": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "primaryContactId": { "type": "integer", @@ -273,7 +295,8 @@ "apiSummary": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "rights": { "type": "string", @@ -281,14 +304,16 @@ "multilingual": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "seq": { "type": "integer", "default": 0, "validation": [ "nullable" - ] + ], + "isPublic": true }, "source": { "type": "string", @@ -332,11 +357,13 @@ ] } } - } + }, + "isPublic": true }, "submissionId": { "type": "integer", - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "subtitle": { "type": "string", @@ -346,7 +373,8 @@ "validation": [ "nullable", "no_new_line" - ] + ], + "isPublic": true }, "supportingAgencies": { "type": "array", @@ -376,6 +404,16 @@ } } }, + "status": { + "type": "integer", + "description": "Whether the publication is Queued (not yet scheduled for publication), Declined, Published or Scheduled (scheduled for publication at a future date). One of the `PKPSubmission::STATUS_*` constants. Default is `PKPSubmission::STATUS_QUEUED`.", + "apiSummary": true, + "default": 1, + "validation": [ + "in:1,3,4,5" + ], + "isPublic": true + }, "title": { "type": "string", "description": "The title without a prefix or subtitle. This may include inline HTML tags, such as `` and ``.", @@ -384,7 +422,8 @@ "validation": [ "nullable", "no_new_line" - ] + ], + "isPublic": true }, "type": { "type": "string", @@ -392,7 +431,8 @@ "multilingual": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "urlPath": { "type": "string", @@ -406,7 +446,8 @@ "type": "string", "description": "The public URL for this publication or where it will be available if it has not yet been published.", "apiSummary": true, - "readOnly": true + "readOnly": true, + "isPublic": true }, "versionMajor": { "type": "integer", @@ -414,7 +455,8 @@ "apiSummary": true, "validation": [ "min:1" - ] + ], + "isPublic": true }, "versionMinor": { "type": "integer", @@ -422,7 +464,8 @@ "apiSummary": true, "validation": [ "min:0" - ] + ], + "isPublic": true }, "versionStage": { "type": "string", @@ -430,13 +473,15 @@ "apiSummary": true, "validation": [ "nullable" - ] + ], + "isPublic": true }, "versionString": { "type": "string", "description": "The calculated display string of the version of this publication", "apiSummary": true, - "readOnly": true + "readOnly": true, + "isPublic": true } } -} \ No newline at end of file +} diff --git a/schemas/submission.json b/schemas/submission.json index 8f1ecc3d384..d28c47207b1 100644 --- a/schemas/submission.json +++ b/schemas/submission.json @@ -9,7 +9,8 @@ "type": "string", "description": "A URL to this object in the REST API.", "readOnly": true, - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "availableEditorialDecisions": { "description": "The editorial decisions that are available for this stage.", @@ -45,13 +46,15 @@ "type": "integer", "description": "The id of the context this submission was created in.", "apiSummary": true, - "writeDisabledInApi": true + "writeDisabledInApi": true, + "isPublic": true }, "currentPublicationId": { "type": "integer", "description": "Which publication is the latest published version.", "apiSummary": true, - "writeDisabledInApi": true + "writeDisabledInApi": true, + "isPublic": true }, "dateLastActivity": { "type": "string", @@ -69,7 +72,8 @@ "writeDisabledInApi": true, "validation": [ "date:Y-m-d H:i:s" - ] + ], + "isPublic": true }, "editorAssigned": { "type": "boolean", @@ -81,7 +85,8 @@ "type": "integer", "description": "The id of this submission.", "readOnly": true, - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "lastModified": { "type": "string", @@ -90,7 +95,8 @@ "writeDisabledInApi": true, "validation": [ "date:Y-m-d H:i:s" - ] + ], + "isPublic": true }, "locale": { "type": "string", @@ -98,7 +104,8 @@ "apiSummary": true, "validation": [ "regex:/^([A-Za-z]{2,4})(?[_-]([A-Za-z]{4,5}|[0-9]{4}))?([_-]([A-Za-z]{2}|[0-9]{3}))?(@[a-z]{2,30}(?&sc)?)?$/" - ] + ], + "isPublic": true }, "metadataLocales": { "type": "array", @@ -106,7 +113,8 @@ "readOnly": true, "items": { "type": "string" - } + }, + "isPublic": true }, "publications": { "type": "array", @@ -117,6 +125,15 @@ "$ref": "#/definitions/Publication" } }, + "currentPublication": { + "type": "array", + "description": "The current publication of a submission", + "readOnly": true, + "isPublic": true, + "items": { + "$ref": "#/definitions/Publication" + } + }, "recommendationsIn": { "type": "boolean", "description": "Whether all recommending editors have submitted a recommendation.", @@ -349,13 +366,15 @@ "writeDisabledInApi": true, "validation": [ "in:1,2,3,4,5" - ] + ], + "isPublic": true }, "statusLabel": { "type": "string", "description": "A human-readable version of the submission's status. It will be Published, Declined, Queued (still in the workflow) or Scheduled.", "readOnly": true, - "apiSummary": true + "apiSummary": true, + "isPublic": true }, "submissionProgress": { "type": "string", @@ -400,7 +419,8 @@ "type": "string", "description": "A URL to view the published version of this submission. If it is not published, the URL will point to the location where it will be published in the future.", "apiSummary": true, - "readOnly": true + "readOnly": true, + "isPublic": true } } }