From b3ad21ac30a93ae7e9d8f87a42c01a49676f15c4 Mon Sep 17 00:00:00 2001 From: MrRob Date: Fri, 12 Sep 2025 12:11:10 +0100 Subject: [PATCH 01/12] files ported from poc and basic implementation of submissions, publications, and navigations --- .../navigations/PKPNavigationController.php | 88 ++++ .../submissions/PKPSubmissionController.php | 394 ++++++------------ classes/core/maps/Schema.php | 8 +- classes/facades/Repo.php | 6 + classes/navigationMenu/Repository.php | 54 +++ .../maps/NavigationItemSchema.php | 107 +++++ classes/navigationMenu/maps/Schema.php | 119 ++++++ classes/publication/maps/Schema.php | 4 +- classes/services/PKPSchemaService.php | 15 +- classes/submission/maps/Schema.php | 85 ++-- schemas/navigationMenu.json | 57 +++ schemas/navigationMenuItem.json | 60 +++ 12 files changed, 689 insertions(+), 308 deletions(-) create mode 100644 api/v1/navigations/PKPNavigationController.php create mode 100644 classes/navigationMenu/Repository.php create mode 100644 classes/navigationMenu/maps/NavigationItemSchema.php create mode 100644 classes/navigationMenu/maps/Schema.php create mode 100644 schemas/navigationMenu.json create mode 100644 schemas/navigationMenuItem.json diff --git a/api/v1/navigations/PKPNavigationController.php b/api/v1/navigations/PKPNavigationController.php new file mode 100644 index 00000000000..3f4f8399f3d --- /dev/null +++ b/api/v1/navigations/PKPNavigationController.php @@ -0,0 +1,88 @@ +getPublic(...)) + ->name('navigation.get') + ->whereNumber('navigationId'); + } + + /** + * Get navigation menu by ID with formatted menu items and nesting + */ + public function getPublic(Request $illuminateRequest): JsonResponse + { + $navigationId = (int) $illuminateRequest->route('navigationId'); + $request = $this->getRequest(); + $context = $request->getContext(); + $contextId = $context->getId(); + $navigationMenu = Repo::navigationMenu()->get($navigationId, $contextId); + + if (!$navigationMenu) { + return response()->json([ + 'error' => 'Navigation menu not found' + ], Response::HTTP_NOT_FOUND); + } + + $mappedNavigation = Repo::navigationMenu()->getSchemaMap()->map( + $navigationMenu, + isPublic: true, + ); + + return response()->json($mappedNavigation, Response::HTTP_OK); + } + +} diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index 0d9a7e4876e..9d190be411f 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -18,7 +18,6 @@ namespace PKP\API\v1\submissions; use APP\author\Author; -use PKP\publication\PKPPublication; use APP\core\Application; use APP\facades\Repo; use APP\mail\variables\ContextEmailVariable; @@ -31,7 +30,6 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator; @@ -56,8 +54,6 @@ use PKP\db\DAORegistry; use PKP\decision\DecisionType; use PKP\editorialTask\EditorialTask; -use PKP\editorialTask\enums\EditorialTaskType; -use PKP\editorialTask\Participant; use PKP\jobs\orcid\SendAuthorMail; use PKP\log\event\PKPSubmissionEventLogEntry; use PKP\mail\mailables\PublicationVersionNotify; @@ -82,7 +78,6 @@ use PKP\security\Role; use PKP\security\Validation; use PKP\services\PKPSchemaService; -use PKP\stageAssignment\StageAssignment; use PKP\submission\GenreDAO; use PKP\submission\PKPSubmission; use PKP\submission\reviewAssignment\ReviewAssignment; @@ -167,6 +162,11 @@ class PKPSubmissionController extends PKPBaseController Role::ROLE_ID_ASSISTANT ]; + public array $publicAccessRoutes = [ + 'getPublic', + 'getPublicationPublic', + ]; + /** * @copydoc \PKP\core\PKPBaseController::getHandlerPath() */ @@ -181,7 +181,6 @@ public function getHandlerPath(): string public function getRouteGroupMiddleware(): array { return [ - 'has.user', 'has.context', ]; } @@ -398,7 +397,7 @@ public function getGroupRoutes(): void ->whereNumber(['submissionId', 'taskId']); Route::delete('{submissionId}/tasks/{taskId}', $this->deleteTask(...)) - ->name('submission.task.delete') + ->name('submission.task.delete ') ->whereNumber(['submissionId', 'taskId']); Route::get('{submissionId}/tasks/{taskId}', $this->getTask(...)) @@ -408,19 +407,15 @@ public function getGroupRoutes(): void Route::get('{submissionId}/stage/{stageId}/tasks', $this->getTasks(...)) ->name('submission.task.getMany') ->whereNumber(['submissionId', 'stageId']); + }); - Route::put('{submissionId}/tasks/{taskId}/close', $this->closeTask(...)) - ->name('submission.task.close') - ->whereNumber(['submissionId', 'taskId']); - - Route::put('{submissionId}/tasks/{taskId}/open', $this->openTask(...)) - ->name('submission.task.open') - ->whereNumber(['submissionId', 'taskId']); + Route::get('{submissionId}/public', $this->getPublic(...)) + ->name('submission.public/get') + ->whereNumber(['submissionId']); - Route::put('{submissionId}/tasks/{taskId}/start', $this->startTask(...)) - ->name('submission.task.start') - ->whereNumber(['submissionId', 'taskId']); - }); + Route::get('{submissionId}/publications/{publicationId}/public', $this->getPublicationPublic(...)) + ->name('submission.publication.public/get') + ->whereNumber(['submissionId', 'publicationId']); } /** @@ -431,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)); @@ -474,7 +470,7 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme } // To modify a task, need to check read and write access policies - if (in_array($actionName, ['editTask', 'deleteTask', 'closeTask', 'openTask', 'startTask'])) { + 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)); @@ -525,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(), @@ -619,6 +615,70 @@ protected function getSubmissionCollector(array $queryParams): Collector return $collector; } + public function getPublic(Request $illuminateRequest): JsonResponse + { + $submissionId = (int) $illuminateRequest->route('submissionId'); + $submission = Repo::submission()->get($submissionId); + + if (!$submission) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + // TODO: Check if should be visible + + // Get required items for submission mapping + $userGroups = UserGroup::withContextIds($submission->getData('contextId'))->cursor(); + + /** @var GenreDAO $genreDao */ + $genreDao = DAORegistry::getDAO('GenreDAO'); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); + + $mappedSubmission = Repo::submission()->getSchemaMap()->map( + $submission, + $userGroups, + $genres, + [], + isPublic: true, + ); + + 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); + } + + $userGroups = UserGroup::withContextIds($submission->getData('contextId'))->get(); + + /** @var GenreDAO $genreDao */ + $genreDao = DAORegistry::getDAO('GenreDAO'); + $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); + + return response()->json( + Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map( + $publication, + isPublic: true, + ), + Response::HTTP_OK + ); + } + /** * Get a single submission */ @@ -636,7 +696,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, @@ -771,7 +831,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(); @@ -814,7 +874,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); @@ -867,7 +927,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); @@ -948,7 +1008,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(); @@ -981,7 +1041,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); @@ -1016,7 +1076,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); } @@ -1207,7 +1267,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(), @@ -1240,7 +1300,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), @@ -1279,7 +1339,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), @@ -1337,13 +1397,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; } @@ -1363,7 +1423,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), @@ -1394,7 +1454,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); @@ -1411,15 +1471,9 @@ public function editPublication(Request $illuminateRequest): JsonResponse $params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_PUBLICATION, $illuminateRequest->input()); $params['id'] = $publication->getId(); - if (array_key_exists('status', $params) && is_null($params['status'])) { - unset($params['status']); - } - - // Only allow to update the status if it's a pre-publish status - // For the publishing statuses, the `/publish` and /unpublish endpoints should be used instead. - if (array_key_exists('status', $params) - && !in_array($params['status'], Publication::getPrePublishStatuses())) { - + // 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)) { return response()->json([ 'error' => __('api.publication.403.cantEditStatus'), ], Response::HTTP_FORBIDDEN); @@ -1436,20 +1490,6 @@ public function editPublication(Request $illuminateRequest): JsonResponse return response()->json($errors, Response::HTTP_BAD_REQUEST); } - // update of version information at publication edit - if ($illuminateRequest->has('versionStage') && $illuminateRequest->has('versionIsMinor')) { - - $versionStage = $this->validateVersionStage($illuminateRequest); - $versionIsMinor = $this->validateVersionIsMinor($illuminateRequest); - - // will only allow to update the version details at publication edit - // if there is no version information added yet for this publication - // or if the given version stage information is different from what already assigned - if (!$publication->getData('versionStage') || $publication->getData('versionStage') !== $versionStage->value) { - $publication = Repo::publication()->updateVersion($publication, $versionStage, $versionIsMinor); - } - } - Repo::publication()->edit($publication, $params); $publication = Repo::publication()->get($publication->getId()); @@ -1457,7 +1497,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), @@ -1490,7 +1530,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); @@ -1518,7 +1558,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), @@ -1546,7 +1586,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); @@ -1561,7 +1601,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), @@ -1593,7 +1633,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); @@ -1603,7 +1643,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); @@ -1647,7 +1687,7 @@ public function getContributor(Request $illuminateRequest): JsonResponse } return response()->json( - Repo::author()->getSchemaMap($submission)->map($author), + Repo::author()->getSchemaMap()->map($author), Response::HTTP_OK ); } @@ -1678,7 +1718,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); } @@ -1708,7 +1748,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); @@ -1778,7 +1818,7 @@ public function addContributor(Request $illuminateRequest): JsonResponse } return response()->json( - Repo::author()->getSchemaMap($submission)->map($author), + Repo::author()->getSchemaMap()->map($author), Response::HTTP_OK ); } @@ -1802,7 +1842,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); @@ -1826,7 +1866,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); @@ -1864,7 +1904,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); @@ -1926,7 +1966,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 ); } @@ -1957,7 +1997,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); @@ -1979,7 +2019,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( @@ -2039,11 +2079,10 @@ public function addTask(AddTask $illuminateRequest): JsonResponse $editorialTask = new EditorialTask($validated); $editorialTask->save(); - $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ - $editorialTask->refresh(); return response()->json( - new TaskResource(resource: $editorialTask, data: $this->getTaskData($submission, $editorialTask)), + (new TaskResource($editorialTask->refresh())) + ->toArray($illuminateRequest), Response::HTTP_OK ); } @@ -2054,7 +2093,6 @@ public function addTask(AddTask $illuminateRequest): JsonResponse public function getTask(Request $illuminateRequest): JsonResponse { $editTask = EditorialTask::find($illuminateRequest->route('taskId')); - $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ if (!$editTask) { return response()->json([ @@ -2063,7 +2101,7 @@ public function getTask(Request $illuminateRequest): JsonResponse } return response()->json( - (new TaskResource(resource: $editTask, data: $this->getTaskData($submission, $editTask))), + (new TaskResource($editTask))->toArray($illuminateRequest), Response::HTTP_OK ); } @@ -2075,19 +2113,18 @@ public function getTasks(Request $illuminateRequest): JsonResponse { $currentUser = $this->getRequest()->getUser(); $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ - $stageId = (int) $illuminateRequest->route('stageId'); // 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($stageId); + ->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($stageId); + ->withStageId($illuminateRequest->route('stageId')); } foreach ($illuminateRequest->query() as $param => $val) { @@ -2099,41 +2136,9 @@ public function getTasks(Request $illuminateRequest): JsonResponse } $tasks = $collector->get(); - $taskIds = $tasks->pluck('id')->toArray(); - - // Get task participants and creators - $participantIds = Participant::withTaskIds($taskIds)->get()->pluck('userId')->merge( - $tasks->pluck('createdBy') - )->filter()->unique()->toArray(); - - $users = Repo::user()->getCollector()->filterByUserIds($participantIds)->getMany(); - - $stageAssignments = StageAssignment::with('userGroup') - ->withSubmissionIds([$submission->getId()]) - ->withStageIds([$stageId]) - ->get(); - - - $userGroups = UserGroup::with('userUserGroups') - ->withContextIds($submission->getData('contextId')) - ->withUserIds($participantIds) - ->get(); - - $reviewAssignments = in_array($stageId, Application::get()->getReviewStages()) ? Repo::reviewAssignment()->getCollector() - ->filterBySubmissionIds([$submission->getId()]) - ->filterByReviewerIds($participantIds) - ->filterByStageId($stageId) - ->getMany() : - collect()->lazy(); return response()->json([ - 'items' => TaskResource::collection(resource: $tasks, data: [ - 'submission' => $submission, - 'stageAssignments' => $stageAssignments, - 'users' => $users, - 'userGroups' => $userGroups, - 'reviewAssignments' => $reviewAssignments, - ]), + 'items' => TaskResource::collection($tasks), 'itemMax' => $tasks->count(), ], Response::HTTP_OK); } @@ -2143,8 +2148,7 @@ public function getTasks(Request $illuminateRequest): JsonResponse */ public function editTask(EditTask $illuminateRequest): JsonResponse { - $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ - $editTask = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_QUERY); /** @var EditorialTask $editTask */ + $editTask = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_QUERY); if (!$editTask) { return response()->json([ @@ -2161,7 +2165,8 @@ public function editTask(EditTask $illuminateRequest): JsonResponse } return response()->json( - new TaskResource(resource: $editTask->refresh(), data: $this->getTaskData($submission, $editTask)), + (new TaskResource($editTask->refresh())) + ->toArray($illuminateRequest), Response::HTTP_OK ); } @@ -2184,137 +2189,16 @@ public function deleteTask(Request $illuminateRequest): JsonResponse return response()->json([], Response::HTTP_OK); } - /** - * Close a task or discussion; closed task cannot be edited anymore - */ - public function closeTask(Request $illuminateRequest): JsonResponse - { - $editTask = EditorialTask::find($illuminateRequest->route('taskId')); - $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ - - if (!$editTask) { - return response()->json([ - 'error' => __('api.404.resourceNotFound'), - ], Response::HTTP_NOT_FOUND); - } - - if ($editTask->dateClosed) { - return response()->json([ - 'error' => __('api.409.resourceActionConflict'), - ], Response::HTTP_CONFLICT); - } - - $editTask->fill(['dateClosed' => Carbon::now()])->save(); - $editTask->refresh(); - return response()->json( - new TaskResource(resource: $editTask, data: $this->getTaskData($submission, $editTask)), - Response::HTTP_OK - ); - } - - /** - * Re-open a closed task or discussion - */ - public function openTask(Request $illuminateRequest): JsonResponse - { - $editTask = EditorialTask::find($illuminateRequest->route('taskId')); - $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ - - if (!$editTask) { - return response()->json([ - 'error' => __('api.404.resourceNotFound'), - ], Response::HTTP_NOT_FOUND); - } - - if (!$editTask->dateClosed) { - return response()->json([ - 'error' => __('api.409.resourceActionConflict'), - ], Response::HTTP_CONFLICT); - } - - $editTask->fill(['dateClosed' => null])->save(); - $editTask->refresh(); - return response()->json( - new TaskResource(resource: $editTask, data: $this->getTaskData($submission, $editTask)), - Response::HTTP_OK - ); - } - - /** - * Start a task or discussion; once started, it cannot be unstarted - */ - public function startTask(Request $illuminateRequest): JsonResponse - { - $editTask = EditorialTask::find($illuminateRequest->route('taskId')); - $submission = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION); /** @var Submission $submission */ - - if (!$editTask) { - return response()->json([ - 'error' => __('api.404.resourceNotFound'), - ], Response::HTTP_NOT_FOUND); - } - - if ($editTask->dateStarted || $editTask->type === EditorialTaskType::DISCUSSION->value) { - return response()->json([ - 'error' => __('api.409.resourceActionConflict'), - ], Response::HTTP_CONFLICT); - } - - $editTask->fill(['dateStarted' => Carbon::now()])->save(); - $editTask->refresh(); - return response()->json( - new TaskResource(resource: $editTask, data: $this->getTaskData($submission, $editTask)), - Response::HTTP_OK - ); - } - - /** - * Get task related data to supply the editorial task and editorial tasl participants resource - */ - protected function getTaskData(Submission $submission, EditorialTask $editTask): array - { - $stageAssignments = StageAssignment::with('userGroup') - ->withSubmissionIds([$submission->getId()]) - ->withStageIds([$editTask->stageId]) - ->get(); - - $participantIds = $editTask->participants()->get()->pluck('userId')->unique()->toArray(); - $creatorId = $editTask->createdBy; - if (!in_array($creatorId, $participantIds)) { - $participantIds[] = $creatorId; - } - - $users = Repo::user()->getCollector()->filterByUserIds($participantIds)->getMany(); - $userGroups = UserGroup::with('userUserGroups') - ->withContextIds($submission->getData('contextId')) - ->withUserIds($participantIds) - ->get(); - $reviewAssignments = in_array($editTask->stageId, Application::get()->getReviewStages()) ? Repo::reviewAssignment()->getCollector() - ->filterBySubmissionIds([$submission->getId()]) - ->filterByReviewerIds($participantIds) - ->filterByStageId($editTask->stageId) - ->getMany() : - collect()->lazy(); - - return [ - 'submission' => $submission, - 'stageAssignments' => $stageAssignments, - 'users' => $users, - 'userGroups' => $userGroups, - 'reviewAssignments' => $reviewAssignments, - ]; - } - /** * 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) + ) ); } @@ -2468,7 +2352,7 @@ protected function getPublicationIdentifierForm(Request $illuminateRequest): Jso /** * Get Publication TitleAbstract Form component - */ + */ protected function getPublicationTitleAbstractForm(Request $illuminateRequest): JsonResponse { $data = $this->getSubmissionAndPublicationData($illuminateRequest); @@ -2517,7 +2401,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()) @@ -2667,7 +2551,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' => [ @@ -2683,7 +2567,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/facades/Repo.php b/classes/facades/Repo.php index 0b384cd5447..8ae864a4dd3 100644 --- a/classes/facades/Repo.php +++ b/classes/facades/Repo.php @@ -42,6 +42,7 @@ use PKP\job\repositories\Job as JobRepository; use PKP\log\event\Repository as EventLogRepository; use PKP\log\Repository as EmailLogEntryRepository; +use PKP\navigationMenu\Repository as NavigationMenuRepository; use PKP\note\Repository as NoteRepository; use PKP\notification\Repository as NotificationRepository; use PKP\ror\Repository as RorRepository; @@ -159,6 +160,11 @@ public static function notification(): NotificationRepository return app(NotificationRepository::class); } + public static function navigationMenu(): NavigationMenuRepository + { + return app(NavigationMenuRepository::class); + } + public static function note(): NoteRepository { return app(NoteRepository::class); diff --git a/classes/navigationMenu/Repository.php b/classes/navigationMenu/Repository.php new file mode 100644 index 00000000000..a075b089103 --- /dev/null +++ b/classes/navigationMenu/Repository.php @@ -0,0 +1,54 @@ +dao = $dao; + } + + /** @copydoc DAO::newDataObject() */ + public function newDataObject(array $params = []): NavigationMenu + { + $object = $this->dao->newDataObject(); + if (!empty($params)) { + $object->setAllData($params); + } + return $object; + } + + /** + * Get an instance of the map class for mapping + * navigation menus to their schema + */ + public function getSchemaMap(): maps\Schema + { + return app('maps')->withExtensions($this->schemaMap); + } + + /** @copydoc DAO::get() */ + public function get(int $id, ?int $contextId = null): ?NavigationMenu + { + return $this->dao->getById($id, $contextId); + } +} diff --git a/classes/navigationMenu/maps/NavigationItemSchema.php b/classes/navigationMenu/maps/NavigationItemSchema.php new file mode 100644 index 00000000000..5d42fa76219 --- /dev/null +++ b/classes/navigationMenu/maps/NavigationItemSchema.php @@ -0,0 +1,107 @@ +mapByProperties($this->getProps(), $item); + } + + /** + * Summarize a NavigationMenuItem + * + * Includes properties with the apiSummary flag in the navigationItem schema. + */ + public function summarize(NavigationMenuItem $item): array + { + return $this->mapByProperties($this->getSummaryProps(), $item); + } + + /** + * Map a collection of NavigationMenuItems + * + * @see self::map + */ + public function mapMany(Enumerable $collection): Enumerable + { + $this->collection = $collection; + return $collection->map([$this, 'map']); + } + + /** + * Summarize a collection of NavigationMenuItems + * + * @see self::summarize + */ + public function summarizeMany(Enumerable $collection): Enumerable + { + $this->collection = $collection; + return $collection->map([$this, 'summarize']); + } + + /** + * Map schema properties of a NavigationMenuItem to an assoc array + */ + protected function mapByProperties(array $props, NavigationMenuItem $navigationItem): array + { + $output = []; + + foreach ($props as $prop) { + switch ($prop) { + case 'id': + $output[$prop] = $navigationItem->getId(); + break; + case 'title': + $output[$prop] = $navigationItem->getData('titleLocaleKey'); + break; + case 'path': + $output[$prop] = $navigationItem->getPath(); + break; + case 'type': + $output[$prop] = $navigationItem->getType(); + break; + case 'sequence': + $output[$prop] = 0; + break; + case 'children': + $output[$prop] = []; + break; + } + } + + $locales = $this->context ? $this->context->getSupportedLocales() : []; + + $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $locales); + + ksort($output); + + return $this->withExtensions($output, $navigationItem); + } +} diff --git a/classes/navigationMenu/maps/Schema.php b/classes/navigationMenu/maps/Schema.php new file mode 100644 index 00000000000..cd63af21831 --- /dev/null +++ b/classes/navigationMenu/maps/Schema.php @@ -0,0 +1,119 @@ +mapByProperties($this->getProps($isPublic), $item); + } + + /** + * Summarize a NavigationMenu + * + * Includes properties with the apiSummary flag in the navigation schema. + */ + public function summarize(NavigationMenu $item): array + { + return $this->mapByProperties($this->getSummaryProps(), $item); + } + + /** + * Map a collection of NavigationMenus + * + * @see self::map + */ + public function mapMany(Enumerable $collection): Enumerable + { + $this->collection = $collection; + return $collection->map([$this, 'map']); + } + + /** + * Summarize a collection of NavigationMenus + * + * @see self::summarize + */ + public function summarizeMany(Enumerable $collection): Enumerable + { + $this->collection = $collection; + return $collection->map([$this, 'summarize']); + } + + /** + * Map schema properties of a NavigationMenu to an assoc array + */ + protected function mapByProperties(array $props, NavigationMenu $navigationMenu): array + { + $output = []; + + foreach ($props as $prop) { + switch ($prop) { + case '_href': + $output[$prop] = $this->getApiUrl( + 'navigations/' . $navigationMenu->getId() . '/public' + ); + break; + case 'id': + $output[$prop] = $navigationMenu->getId(); + break; + case 'title': + $output[$prop] = $navigationMenu->getTitle(); + break; + case 'area_name': + $output[$prop] = $navigationMenu->getAreaName(); + break; + case 'context_id': + $output[$prop] = $navigationMenu->getContextId(); + break; + case 'items': + $navigationMenuItemDao = DAORegistry::getDAO('NavigationMenuItemDAO'); /** @var NavigationMenuItemDAO $navigationMenuItemDao */ + $navigationMenuItems = $navigationMenuItemDao->getByMenuId($navigationMenu->getId()); + $items = []; + while ($navigationMenuItem = $navigationMenuItems->next()) { + /** @var NavigationMenuItem $navigationMenuItem */ + $navigationItemSchema = new NavigationItemSchema($this->request, $this->context, $this->schemaService); + $items[] = $navigationItemSchema->summarize($navigationMenuItem); + } + $output[$prop] = $items; + break; + } + } + + $locales = $this->context ? $this->context->getSupportedLocales() : []; + $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $locales); + + ksort($output); + + return $this->withExtensions($output, $navigationMenu); + } +} diff --git a/classes/publication/maps/Schema.php b/classes/publication/maps/Schema.php index 02dc0159c1c..f2986d9c286 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); } /** diff --git a/classes/services/PKPSchemaService.php b/classes/services/PKPSchemaService.php index ad24466a757..fa57de140d9 100644 --- a/classes/services/PKPSchemaService.php +++ b/classes/services/PKPSchemaService.php @@ -41,6 +41,8 @@ class PKPSchemaService public const SCHEMA_HIGHLIGHT = 'highlight'; public const SCHEMA_INSTITUTION = 'institution'; public const SCHEMA_ISSUE = 'issue'; + public const SCHEMA_NAVIGATION_MENU = 'navigationMenu'; + public const SCHEMA_NAVIGATION_MENU_ITEM = 'navigationMenuItem'; public const SCHEMA_PUBLICATION = 'publication'; public const SCHEMA_REVIEW_ASSIGNMENT = 'reviewAssignment'; public const SCHEMA_REVIEW_ROUND = 'reviewRound'; @@ -188,14 +190,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 addab1eb5f3..ac86a703d82 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 * @@ -141,7 +141,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 +154,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 +220,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 +316,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 +326,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 +403,6 @@ public function mapManyToSubmissionsList( $associatedStageAssignments->get($item->getId()), $associatedDecisions->get($item->getId()), $anonymizeReviews, - $associatedReviewerSuggestions->get($item->getId()), $associatedSubmissionStageFiles->get($item->getId()) ) ); @@ -459,7 +457,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 +466,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 +489,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()) @@ -506,16 +504,16 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co ->summarizeMany($submission->getData('publications'), $anonymize)->values(); 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 +546,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 +560,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 +581,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 +631,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 ( @@ -827,7 +824,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(); @@ -847,7 +844,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. @@ -862,7 +859,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) { @@ -927,7 +924,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() && @@ -1038,9 +1035,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; @@ -1054,9 +1051,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 @@ -1081,8 +1078,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; } @@ -1127,7 +1123,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*/ @@ -1200,8 +1196,7 @@ protected function getStageFilesBySubmissions(Enumerable $submissions, array $st ->getCollector() ->filterBySubmissionIds($submissionIds) ->filterByFileStages($stageIds) - ->getMany() - ->remember(); + ->getMany(); } /** diff --git a/schemas/navigationMenu.json b/schemas/navigationMenu.json new file mode 100644 index 00000000000..672ac1b2744 --- /dev/null +++ b/schemas/navigationMenu.json @@ -0,0 +1,57 @@ +{ + "title": "Navigation", + "description": "A navigation menu containing structured menu items.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "_href": { + "type": "string", + "format": "uri", + "readOnly": true, + "apiSummary": true, + "isPublic": true + }, + "id": { + "type": "integer", + "readOnly": true, + "apiSummary": true, + "isPublic": true + }, + "title": { + "type": "string", + "description": "The title of the navigation menu.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "area_name": { + "type": "string", + "description": "The area where this navigation menu is displayed.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "context_id": { + "type": "integer", + "description": "The ID of the context this navigation menu belongs to.", + "apiSummary": true, + "isPublic": true + }, + "items": { + "type": "array", + "description": "The navigation items in this menu.", + "apiSummary": true, + "readOnly": true, + "items": { + "$ref": "#/definitions/NavigationItem" + }, + "isPublic": true + } + } +} diff --git a/schemas/navigationMenuItem.json b/schemas/navigationMenuItem.json new file mode 100644 index 00000000000..9990813cd5c --- /dev/null +++ b/schemas/navigationMenuItem.json @@ -0,0 +1,60 @@ +{ + "title": "NavigationItem", + "description": "A single navigation menu item.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "apiSummary": true, + "isPublic": true + }, + "title": { + "type": "string", + "description": "The display title of the navigation item.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "path": { + "type": "string", + "description": "The path or URL for this navigation item.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "type": { + "type": "string", + "description": "The type of navigation item (e.g., 'link', 'page', 'custom').", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "sequence": { + "type": "integer", + "description": "The sort order of this item within its parent level.", + "apiSummary": true, + "default": 0, + "isPublic": true + }, + "children": { + "type": "array", + "description": "Child navigation items for hierarchical menus.", + "apiSummary": true, + "readOnly": true, + "items": { + "$ref": "#/definitions/NavigationItem" + }, + "isPublic": true + } + } +} From b98e933a340c55080119ba8723b158cfb2a06790 Mon Sep 17 00:00:00 2001 From: MrRob Date: Fri, 12 Sep 2025 12:53:18 +0100 Subject: [PATCH 02/12] added status to publication schema --- schemas/publication.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/schemas/publication.json b/schemas/publication.json index 5dc1a9c5727..c8d58643d14 100644 --- a/schemas/publication.json +++ b/schemas/publication.json @@ -437,6 +437,11 @@ "description": "The calculated display string of the version of this publication", "apiSummary": true, "readOnly": true + }, + "status": { + "type": "integer", + "description": "The status of the publication", + "apiSummary": true } } } \ No newline at end of file From 95882e63aafc27ecc5ec4379716d5fe4da870cfd Mon Sep 17 00:00:00 2001 From: MrRob Date: Wed, 17 Sep 2025 11:46:27 +0100 Subject: [PATCH 03/12] retrieving current publication with submission found via id or url_path --- .../submissions/PKPSubmissionController.php | 31 ++++---------- classes/submission/maps/Schema.php | 15 +++++++ schemas/publication.json | 8 ++-- schemas/submission.json | 42 ++++++++++++++----- 4 files changed, 58 insertions(+), 38 deletions(-) diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index 9d190be411f..c5235720002 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -411,7 +411,7 @@ public function getGroupRoutes(): void Route::get('{submissionId}/public', $this->getPublic(...)) ->name('submission.public/get') - ->whereNumber(['submissionId']); + ->where('submissionId', '[0-9]+|[a-zA-Z0-9\-_]+'); Route::get('{submissionId}/publications/{publicationId}/public', $this->getPublicationPublic(...)) ->name('submission.publication.public/get') @@ -617,31 +617,14 @@ protected function getSubmissionCollector(array $queryParams): Collector public function getPublic(Request $illuminateRequest): JsonResponse { - $submissionId = (int) $illuminateRequest->route('submissionId'); - $submission = Repo::submission()->get($submissionId); - - if (!$submission) { - return response()->json([ - 'error' => __('api.404.resourceNotFound'), - ], Response::HTTP_NOT_FOUND); - } - - // TODO: Check if should be visible - - // Get required items for submission mapping - $userGroups = UserGroup::withContextIds($submission->getData('contextId'))->cursor(); + $request = $this->getRequest(); + $urlPath = $illuminateRequest->route('submissionId'); - /** @var GenreDAO $genreDao */ - $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); + $submission = ctype_digit((string) $urlPath) + ? Repo::submission()->get((int) $urlPath, $request->getContext()->getId()) + : Repo::submission()->getByUrlPath($urlPath, $request->getContext()->getId()); - $mappedSubmission = Repo::submission()->getSchemaMap()->map( - $submission, - $userGroups, - $genres, - [], - isPublic: true, - ); + $mappedSubmission = Repo::submission()->getSchemaMap()->mapPublic($submission); return response()->json($mappedSubmission, Response::HTTP_OK); } diff --git a/classes/submission/maps/Schema.php b/classes/submission/maps/Schema.php index ac86a703d82..e4e37f925b6 100644 --- a/classes/submission/maps/Schema.php +++ b/classes/submission/maps/Schema.php @@ -117,6 +117,18 @@ protected function getSubmissionsListProps(): array return $props; } + /** + * Map a submission for the public API + * + */ + public function mapPublic(Submission $item): array + { + $this->userGroups = collect(); + $this->genres = []; + + return $this->mapByProperties($this->getProps(true), $item); + } + /** * Map a submission * @@ -503,6 +515,9 @@ 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->areRecommendationsIn($currentReviewRound, $this->stageAssignments) : null; break; diff --git a/schemas/publication.json b/schemas/publication.json index c8d58643d14..b0452176b5e 100644 --- a/schemas/publication.json +++ b/schemas/publication.json @@ -430,13 +430,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 }, "status": { "type": "integer", @@ -444,4 +446,4 @@ "apiSummary": true } } -} \ No newline at end of file +} diff --git a/schemas/submission.json b/schemas/submission.json index fd27f71cc8b..de8978eab14 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.", @@ -345,13 +362,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", @@ -396,7 +415,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 } } } From e8e800c56f9ccc7387bc86233c499689ac267f8b Mon Sep 17 00:00:00 2001 From: MrRob Date: Fri, 19 Sep 2025 14:07:31 +0100 Subject: [PATCH 04/12] added model for navigation parts --- .../navigations/PKPNavigationController.php | 22 ++- .../resources/NavigationMenuItemResource.php | 55 +++++++ .../resources/NavigationResource.php | 45 ++++++ classes/facades/Repo.php | 6 - classes/navigationMenu/Repository.php | 54 ------- .../maps/NavigationItemSchema.php | 107 -------------- classes/navigationMenu/maps/Schema.php | 119 --------------- .../navigationMenu/models/NavigationMenu.php | 73 ++++++++++ .../models/NavigationMenuItem.php | 137 ++++++++++++++++++ .../models/NavigationMenuItemAssignment.php | 85 +++++++++++ .../models/NavigationMenuItemSetting.php | 63 ++++++++ classes/services/PKPSchemaService.php | 2 - schemas/navigationMenu.json | 57 -------- schemas/navigationMenuItem.json | 60 -------- 14 files changed, 467 insertions(+), 418 deletions(-) create mode 100644 api/v1/navigations/resources/NavigationMenuItemResource.php create mode 100644 api/v1/navigations/resources/NavigationResource.php delete mode 100644 classes/navigationMenu/Repository.php delete mode 100644 classes/navigationMenu/maps/NavigationItemSchema.php delete mode 100644 classes/navigationMenu/maps/Schema.php create mode 100644 classes/navigationMenu/models/NavigationMenu.php create mode 100644 classes/navigationMenu/models/NavigationMenuItem.php create mode 100644 classes/navigationMenu/models/NavigationMenuItemAssignment.php create mode 100644 classes/navigationMenu/models/NavigationMenuItemSetting.php delete mode 100644 schemas/navigationMenu.json delete mode 100644 schemas/navigationMenuItem.json diff --git a/api/v1/navigations/PKPNavigationController.php b/api/v1/navigations/PKPNavigationController.php index 3f4f8399f3d..385bb0a716e 100644 --- a/api/v1/navigations/PKPNavigationController.php +++ b/api/v1/navigations/PKPNavigationController.php @@ -21,8 +21,9 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Route; +use PKP\API\v1\navigations\resources\NavigationResource; use PKP\core\PKPBaseController; -use APP\facades\Repo; +use PKP\navigationMenu\models\NavigationMenu; class PKPNavigationController extends PKPBaseController { @@ -55,9 +56,9 @@ public function getRouteGroupMiddleware(): array */ public function getGroupRoutes(): void { - Route::get('{navigationId}/public', $this->getPublic(...)) + Route::get('{areaName}/public', $this->getPublic(...)) ->name('navigation.get') - ->whereNumber('navigationId'); + ->where('areaName', '[0-9]+|[a-zA-Z0-9\-_]+'); } /** @@ -65,11 +66,8 @@ public function getGroupRoutes(): void */ public function getPublic(Request $illuminateRequest): JsonResponse { - $navigationId = (int) $illuminateRequest->route('navigationId'); - $request = $this->getRequest(); - $context = $request->getContext(); - $contextId = $context->getId(); - $navigationMenu = Repo::navigationMenu()->get($navigationId, $contextId); + $navigationMenu = NavigationMenu::where('area_name', $illuminateRequest->route('areaName')) + ->where('context_id', $this->getRequest()->getContext()->getId())->first(); if (!$navigationMenu) { return response()->json([ @@ -77,12 +75,10 @@ public function getPublic(Request $illuminateRequest): JsonResponse ], Response::HTTP_NOT_FOUND); } - $mappedNavigation = Repo::navigationMenu()->getSchemaMap()->map( - $navigationMenu, - isPublic: true, + return response()->json( + (new NavigationResource($navigationMenu))->toArray($illuminateRequest), + Response::HTTP_OK ); - - return response()->json($mappedNavigation, 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..e3a2642bbd5 --- /dev/null +++ b/api/v1/navigations/resources/NavigationMenuItemResource.php @@ -0,0 +1,55 @@ +getRequest()->getContext(); + $supportedLocales = $context ? $context->getSupportedLocales() : []; + + $titleData = []; + if ($this->titleLocaleKey) { + foreach ($supportedLocales 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/classes/facades/Repo.php b/classes/facades/Repo.php index 8ae864a4dd3..0b384cd5447 100644 --- a/classes/facades/Repo.php +++ b/classes/facades/Repo.php @@ -42,7 +42,6 @@ use PKP\job\repositories\Job as JobRepository; use PKP\log\event\Repository as EventLogRepository; use PKP\log\Repository as EmailLogEntryRepository; -use PKP\navigationMenu\Repository as NavigationMenuRepository; use PKP\note\Repository as NoteRepository; use PKP\notification\Repository as NotificationRepository; use PKP\ror\Repository as RorRepository; @@ -160,11 +159,6 @@ public static function notification(): NotificationRepository return app(NotificationRepository::class); } - public static function navigationMenu(): NavigationMenuRepository - { - return app(NavigationMenuRepository::class); - } - public static function note(): NoteRepository { return app(NoteRepository::class); diff --git a/classes/navigationMenu/Repository.php b/classes/navigationMenu/Repository.php deleted file mode 100644 index a075b089103..00000000000 --- a/classes/navigationMenu/Repository.php +++ /dev/null @@ -1,54 +0,0 @@ -dao = $dao; - } - - /** @copydoc DAO::newDataObject() */ - public function newDataObject(array $params = []): NavigationMenu - { - $object = $this->dao->newDataObject(); - if (!empty($params)) { - $object->setAllData($params); - } - return $object; - } - - /** - * Get an instance of the map class for mapping - * navigation menus to their schema - */ - public function getSchemaMap(): maps\Schema - { - return app('maps')->withExtensions($this->schemaMap); - } - - /** @copydoc DAO::get() */ - public function get(int $id, ?int $contextId = null): ?NavigationMenu - { - return $this->dao->getById($id, $contextId); - } -} diff --git a/classes/navigationMenu/maps/NavigationItemSchema.php b/classes/navigationMenu/maps/NavigationItemSchema.php deleted file mode 100644 index 5d42fa76219..00000000000 --- a/classes/navigationMenu/maps/NavigationItemSchema.php +++ /dev/null @@ -1,107 +0,0 @@ -mapByProperties($this->getProps(), $item); - } - - /** - * Summarize a NavigationMenuItem - * - * Includes properties with the apiSummary flag in the navigationItem schema. - */ - public function summarize(NavigationMenuItem $item): array - { - return $this->mapByProperties($this->getSummaryProps(), $item); - } - - /** - * Map a collection of NavigationMenuItems - * - * @see self::map - */ - public function mapMany(Enumerable $collection): Enumerable - { - $this->collection = $collection; - return $collection->map([$this, 'map']); - } - - /** - * Summarize a collection of NavigationMenuItems - * - * @see self::summarize - */ - public function summarizeMany(Enumerable $collection): Enumerable - { - $this->collection = $collection; - return $collection->map([$this, 'summarize']); - } - - /** - * Map schema properties of a NavigationMenuItem to an assoc array - */ - protected function mapByProperties(array $props, NavigationMenuItem $navigationItem): array - { - $output = []; - - foreach ($props as $prop) { - switch ($prop) { - case 'id': - $output[$prop] = $navigationItem->getId(); - break; - case 'title': - $output[$prop] = $navigationItem->getData('titleLocaleKey'); - break; - case 'path': - $output[$prop] = $navigationItem->getPath(); - break; - case 'type': - $output[$prop] = $navigationItem->getType(); - break; - case 'sequence': - $output[$prop] = 0; - break; - case 'children': - $output[$prop] = []; - break; - } - } - - $locales = $this->context ? $this->context->getSupportedLocales() : []; - - $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $locales); - - ksort($output); - - return $this->withExtensions($output, $navigationItem); - } -} diff --git a/classes/navigationMenu/maps/Schema.php b/classes/navigationMenu/maps/Schema.php deleted file mode 100644 index cd63af21831..00000000000 --- a/classes/navigationMenu/maps/Schema.php +++ /dev/null @@ -1,119 +0,0 @@ -mapByProperties($this->getProps($isPublic), $item); - } - - /** - * Summarize a NavigationMenu - * - * Includes properties with the apiSummary flag in the navigation schema. - */ - public function summarize(NavigationMenu $item): array - { - return $this->mapByProperties($this->getSummaryProps(), $item); - } - - /** - * Map a collection of NavigationMenus - * - * @see self::map - */ - public function mapMany(Enumerable $collection): Enumerable - { - $this->collection = $collection; - return $collection->map([$this, 'map']); - } - - /** - * Summarize a collection of NavigationMenus - * - * @see self::summarize - */ - public function summarizeMany(Enumerable $collection): Enumerable - { - $this->collection = $collection; - return $collection->map([$this, 'summarize']); - } - - /** - * Map schema properties of a NavigationMenu to an assoc array - */ - protected function mapByProperties(array $props, NavigationMenu $navigationMenu): array - { - $output = []; - - foreach ($props as $prop) { - switch ($prop) { - case '_href': - $output[$prop] = $this->getApiUrl( - 'navigations/' . $navigationMenu->getId() . '/public' - ); - break; - case 'id': - $output[$prop] = $navigationMenu->getId(); - break; - case 'title': - $output[$prop] = $navigationMenu->getTitle(); - break; - case 'area_name': - $output[$prop] = $navigationMenu->getAreaName(); - break; - case 'context_id': - $output[$prop] = $navigationMenu->getContextId(); - break; - case 'items': - $navigationMenuItemDao = DAORegistry::getDAO('NavigationMenuItemDAO'); /** @var NavigationMenuItemDAO $navigationMenuItemDao */ - $navigationMenuItems = $navigationMenuItemDao->getByMenuId($navigationMenu->getId()); - $items = []; - while ($navigationMenuItem = $navigationMenuItems->next()) { - /** @var NavigationMenuItem $navigationMenuItem */ - $navigationItemSchema = new NavigationItemSchema($this->request, $this->context, $this->schemaService); - $items[] = $navigationItemSchema->summarize($navigationMenuItem); - } - $output[$prop] = $items; - break; - } - } - - $locales = $this->context ? $this->context->getSupportedLocales() : []; - $output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $locales); - - ksort($output); - - return $this->withExtensions($output, $navigationMenu); - } -} 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/services/PKPSchemaService.php b/classes/services/PKPSchemaService.php index fa57de140d9..c02aaa25968 100644 --- a/classes/services/PKPSchemaService.php +++ b/classes/services/PKPSchemaService.php @@ -41,8 +41,6 @@ class PKPSchemaService public const SCHEMA_HIGHLIGHT = 'highlight'; public const SCHEMA_INSTITUTION = 'institution'; public const SCHEMA_ISSUE = 'issue'; - public const SCHEMA_NAVIGATION_MENU = 'navigationMenu'; - public const SCHEMA_NAVIGATION_MENU_ITEM = 'navigationMenuItem'; public const SCHEMA_PUBLICATION = 'publication'; public const SCHEMA_REVIEW_ASSIGNMENT = 'reviewAssignment'; public const SCHEMA_REVIEW_ROUND = 'reviewRound'; diff --git a/schemas/navigationMenu.json b/schemas/navigationMenu.json deleted file mode 100644 index 672ac1b2744..00000000000 --- a/schemas/navigationMenu.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "title": "Navigation", - "description": "A navigation menu containing structured menu items.", - "type": "object", - "required": [ - "id" - ], - "properties": { - "_href": { - "type": "string", - "format": "uri", - "readOnly": true, - "apiSummary": true, - "isPublic": true - }, - "id": { - "type": "integer", - "readOnly": true, - "apiSummary": true, - "isPublic": true - }, - "title": { - "type": "string", - "description": "The title of the navigation menu.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "area_name": { - "type": "string", - "description": "The area where this navigation menu is displayed.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "context_id": { - "type": "integer", - "description": "The ID of the context this navigation menu belongs to.", - "apiSummary": true, - "isPublic": true - }, - "items": { - "type": "array", - "description": "The navigation items in this menu.", - "apiSummary": true, - "readOnly": true, - "items": { - "$ref": "#/definitions/NavigationItem" - }, - "isPublic": true - } - } -} diff --git a/schemas/navigationMenuItem.json b/schemas/navigationMenuItem.json deleted file mode 100644 index 9990813cd5c..00000000000 --- a/schemas/navigationMenuItem.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "title": "NavigationItem", - "description": "A single navigation menu item.", - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer", - "readOnly": true, - "apiSummary": true, - "isPublic": true - }, - "title": { - "type": "string", - "description": "The display title of the navigation item.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "path": { - "type": "string", - "description": "The path or URL for this navigation item.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "type": { - "type": "string", - "description": "The type of navigation item (e.g., 'link', 'page', 'custom').", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "sequence": { - "type": "integer", - "description": "The sort order of this item within its parent level.", - "apiSummary": true, - "default": 0, - "isPublic": true - }, - "children": { - "type": "array", - "description": "Child navigation items for hierarchical menus.", - "apiSummary": true, - "readOnly": true, - "items": { - "$ref": "#/definitions/NavigationItem" - }, - "isPublic": true - } - } -} From f07a22e2de9b56aa3ac6473a58e22adf89bb6f75 Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 11:42:29 +0100 Subject: [PATCH 05/12] submissions publications schema fix --- classes/submission/maps/Schema.php | 2 + schemas/publication.json | 114 +++++++++++++++++++---------- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/classes/submission/maps/Schema.php b/classes/submission/maps/Schema.php index e4e37f925b6..529c64c4979 100644 --- a/classes/submission/maps/Schema.php +++ b/classes/submission/maps/Schema.php @@ -125,6 +125,8 @@ 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); } diff --git a/schemas/publication.json b/schemas/publication.json index b0452176b5e..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", @@ -439,11 +482,6 @@ "apiSummary": true, "readOnly": true, "isPublic": true - }, - "status": { - "type": "integer", - "description": "The status of the publication", - "apiSummary": true } } } From e07a23d54a6dc82a624f2798c4e4ca8c6dc79f2a Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 12:20:03 +0100 Subject: [PATCH 06/12] merged PKPSubmissionController --- .../submissions/PKPSubmissionController.php | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index c5235720002..f2404183ceb 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -36,9 +36,6 @@ use Illuminate\Support\LazyCollection; use Illuminate\Validation\Rule; use PKP\affiliation\Affiliation; -use PKP\API\v1\submissions\formRequests\AddTask; -use PKP\API\v1\submissions\formRequests\EditTask; -use PKP\API\v1\submissions\resources\TaskResource; use PKP\components\forms\FormComponent; use PKP\components\forms\publication\PKPCitationsForm; use PKP\components\forms\publication\PKPMetadataForm; @@ -69,9 +66,6 @@ use PKP\security\authorization\internal\SubmissionCompletePolicy; use PKP\security\authorization\PublicationAccessPolicy; use PKP\security\authorization\PublicationWritePolicy; -use PKP\security\authorization\QueryAccessPolicy; -use PKP\security\authorization\QueryWorkflowStageAccessPolicy; -use PKP\security\authorization\QueryWritePolicy; use PKP\security\authorization\StageRolePolicy; use PKP\security\authorization\SubmissionAccessPolicy; use PKP\security\authorization\UserRolesRequiredPolicy; @@ -79,7 +73,6 @@ use PKP\security\Validation; use PKP\services\PKPSchemaService; use PKP\submission\GenreDAO; -use PKP\submission\PKPSubmission; use PKP\submission\reviewAssignment\ReviewAssignment; use PKP\submissionFile\SubmissionFile; use PKP\userGroup\UserGroup; @@ -128,11 +121,6 @@ class PKPSubmissionController extends PKPBaseController 'getChangeLanguageMetadata', 'changeVersion', 'getNextAvailableVersion', - 'addTask', - 'editTask', - 'deleteTask', - 'getTask', - 'getTasks', ]; /** @var array Handlers that must be authorized to write to a publication */ @@ -1456,7 +1444,15 @@ public function editPublication(Request $illuminateRequest): JsonResponse // 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)) { + if (array_key_exists('status', $params) && is_null($params['status'])) { + unset($params['status']); + } + + // Only allow to update the status if it's a pre-publish status + // For the publishing statuses, the `/publish` and /unpublish endpoints should be used instead. + if (array_key_exists('status', $params) + && !in_array($params['status'], Publication::getPrePublishStatuses())) { + return response()->json([ 'error' => __('api.publication.403.cantEditStatus'), ], Response::HTTP_FORBIDDEN); @@ -1473,6 +1469,20 @@ public function editPublication(Request $illuminateRequest): JsonResponse return response()->json($errors, Response::HTTP_BAD_REQUEST); } + // update of version information at publication edit + if ($illuminateRequest->has('versionStage') && $illuminateRequest->has('versionIsMinor')) { + + $versionStage = $this->validateVersionStage($illuminateRequest); + $versionIsMinor = $this->validateVersionIsMinor($illuminateRequest); + + // will only allow to update the version details at publication edit + // if there is no version information added yet for this publication + // or if the given version stage information is different from what already assigned + if (!$publication->getData('versionStage') || $publication->getData('versionStage') !== $versionStage->value) { + $publication = Repo::publication()->updateVersion($publication, $versionStage, $versionIsMinor); + } + } + Repo::publication()->edit($publication, $params); $publication = Repo::publication()->get($publication->getId()); @@ -2054,6 +2064,7 @@ public function addDecision(Request $illuminateRequest): JsonResponse } /** +<<<<<<< HEAD * Creates a task or discussion associated with the submission */ public function addTask(AddTask $illuminateRequest): JsonResponse @@ -2173,6 +2184,8 @@ public function deleteTask(Request $illuminateRequest): JsonResponse } /** +======= +>>>>>>> main * Is the current user an editor */ protected function isEditor(): bool From 3ffde23754a712c7c60ff1f36330d85981cdda5c Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 16:18:58 +0100 Subject: [PATCH 07/12] route to get my major and minor version --- .../submissions/PKPSubmissionController.php | 34 +++++++++++++++++++ classes/publication/Repository.php | 23 +++++++++++++ 2 files changed, 57 insertions(+) diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index a4a1deebe1d..3757c36f0d6 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -161,6 +161,7 @@ class PKPSubmissionController extends PKPBaseController public array $publicAccessRoutes = [ 'getPublic', 'getPublicationPublic', + 'getPublicationByVersionPublic' ]; /** @@ -412,6 +413,10 @@ public function getGroupRoutes(): void 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']); } /** @@ -658,6 +663,35 @@ public function getPublicationPublic(Request $illuminateRequest): JsonResponse ); } + 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); + } + + $userGroups = UserGroup::withContextIds($submission->getData('contextId'))->get(); + + return response()->json( + Repo::publication()->getSchemaMap($submission, collect(), [])->map( + $publication, + isPublic: true, + ), + Response::HTTP_OK + ); + +// Route::get('{submissionId}/publications/{versionMajor}/{versionMinor}/public', $this->getPublicationByVersionPublic(...)) + + + } + /** * Get a single submission */ 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 { From 437229a1ccf4d3e56e7bfd6771edf66643ba1ebe Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 16:30:35 +0100 Subject: [PATCH 08/12] removed not needed code --- api/v1/submissions/PKPSubmissionController.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index 3757c36f0d6..8ecd58329d9 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -677,8 +677,6 @@ public function getPublicationByVersionPublic(Request $illuminateRequest): JsonR ], Response::HTTP_NOT_FOUND); } - $userGroups = UserGroup::withContextIds($submission->getData('contextId'))->get(); - return response()->json( Repo::publication()->getSchemaMap($submission, collect(), [])->map( $publication, @@ -687,9 +685,6 @@ public function getPublicationByVersionPublic(Request $illuminateRequest): JsonR Response::HTTP_OK ); -// Route::get('{submissionId}/publications/{versionMajor}/{versionMinor}/public', $this->getPublicationByVersionPublic(...)) - - } /** From 488c9324c99419483ac031156df7250db9a9121b Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 17:01:03 +0100 Subject: [PATCH 09/12] schemas moved to lib/pkp --- schemas/navigation.json | 57 ++++++++++++++++++++++++++++++++++ schemas/navigationItem.json | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 schemas/navigation.json create mode 100644 schemas/navigationItem.json diff --git a/schemas/navigation.json b/schemas/navigation.json new file mode 100644 index 00000000000..5e909d0dcf4 --- /dev/null +++ b/schemas/navigation.json @@ -0,0 +1,57 @@ +{ + "title": "Navigation", + "description": "A navigation menu containing structured menu items.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "_href": { + "type": "string", + "format": "uri", + "readOnly": true, + "apiSummary": true, + "isPublic": true + }, + "id": { + "type": "integer", + "readOnly": true, + "apiSummary": true, + "isPublic": true + }, + "title": { + "type": "string", + "description": "The title of the navigation menu.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "area_name": { + "type": "string", + "description": "The area where this navigation menu is displayed.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "context_id": { + "type": "integer", + "description": "The ID of the context this navigation menu belongs to.", + "apiSummary": true, + "isPublic": true + }, + "items": { + "type": "array", + "description": "The navigation items in this menu.", + "apiSummary": true, + "readOnly": true, + "items": { + "$ref": "#/definitions/NavigationItem" + }, + "isPublic": true + } + } +} \ No newline at end of file diff --git a/schemas/navigationItem.json b/schemas/navigationItem.json new file mode 100644 index 00000000000..45ad375b4ac --- /dev/null +++ b/schemas/navigationItem.json @@ -0,0 +1,61 @@ +{ + "title": "NavigationItem", + "description": "A single navigation menu item.", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "readOnly": true, + "apiSummary": true, + "isPublic": true + }, + "title": { + "type": "string", + "description": "The display title of the navigation item.", + "multilingual": true, + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "path": { + "type": "string", + "description": "The path or URL for this navigation item.", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "type": { + "type": "string", + "description": "The type of navigation item (e.g., 'link', 'page', 'custom').", + "apiSummary": true, + "validation": [ + "nullable" + ], + "isPublic": true + }, + "sequence": { + "type": "integer", + "description": "The sort order of this item within its parent level.", + "apiSummary": true, + "default": 0, + "isPublic": true + }, + "children": { + "type": "array", + "description": "Child navigation items for hierarchical menus.", + "apiSummary": true, + "readOnly": true, + "items": { + "$ref": "#/definitions/NavigationItem" + }, + "isPublic": true + } + } +} \ No newline at end of file From 7f9833f13f35eda6c67863d25246426ff7f7814a Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 17:02:15 +0100 Subject: [PATCH 10/12] navigation schemas removed --- schemas/navigation.json | 57 ---------------------------------- schemas/navigationItem.json | 61 ------------------------------------- 2 files changed, 118 deletions(-) delete mode 100644 schemas/navigation.json delete mode 100644 schemas/navigationItem.json diff --git a/schemas/navigation.json b/schemas/navigation.json deleted file mode 100644 index 5e909d0dcf4..00000000000 --- a/schemas/navigation.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "title": "Navigation", - "description": "A navigation menu containing structured menu items.", - "type": "object", - "required": [ - "id" - ], - "properties": { - "_href": { - "type": "string", - "format": "uri", - "readOnly": true, - "apiSummary": true, - "isPublic": true - }, - "id": { - "type": "integer", - "readOnly": true, - "apiSummary": true, - "isPublic": true - }, - "title": { - "type": "string", - "description": "The title of the navigation menu.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "area_name": { - "type": "string", - "description": "The area where this navigation menu is displayed.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "context_id": { - "type": "integer", - "description": "The ID of the context this navigation menu belongs to.", - "apiSummary": true, - "isPublic": true - }, - "items": { - "type": "array", - "description": "The navigation items in this menu.", - "apiSummary": true, - "readOnly": true, - "items": { - "$ref": "#/definitions/NavigationItem" - }, - "isPublic": true - } - } -} \ No newline at end of file diff --git a/schemas/navigationItem.json b/schemas/navigationItem.json deleted file mode 100644 index 45ad375b4ac..00000000000 --- a/schemas/navigationItem.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "title": "NavigationItem", - "description": "A single navigation menu item.", - "type": "object", - "required": [ - "id" - ], - "properties": { - "id": { - "type": "integer", - "readOnly": true, - "apiSummary": true, - "isPublic": true - }, - "title": { - "type": "string", - "description": "The display title of the navigation item.", - "multilingual": true, - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "path": { - "type": "string", - "description": "The path or URL for this navigation item.", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "type": { - "type": "string", - "description": "The type of navigation item (e.g., 'link', 'page', 'custom').", - "apiSummary": true, - "validation": [ - "nullable" - ], - "isPublic": true - }, - "sequence": { - "type": "integer", - "description": "The sort order of this item within its parent level.", - "apiSummary": true, - "default": 0, - "isPublic": true - }, - "children": { - "type": "array", - "description": "Child navigation items for hierarchical menus.", - "apiSummary": true, - "readOnly": true, - "items": { - "$ref": "#/definitions/NavigationItem" - }, - "isPublic": true - } - } -} \ No newline at end of file From 620e15e46e6297b3064f686a04a0a73a6a41f1af Mon Sep 17 00:00:00 2001 From: MrRob Date: Mon, 22 Sep 2025 21:06:23 +0100 Subject: [PATCH 11/12] get by version and removing user groups --- api/v1/submissions/PKPSubmissionController.php | 9 +-------- classes/publication/maps/Schema.php | 4 ++-- classes/services/PKPSchemaService.php | 12 ++++++++++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/api/v1/submissions/PKPSubmissionController.php b/api/v1/submissions/PKPSubmissionController.php index 8ecd58329d9..52da476278e 100644 --- a/api/v1/submissions/PKPSubmissionController.php +++ b/api/v1/submissions/PKPSubmissionController.php @@ -63,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; @@ -648,14 +647,8 @@ public function getPublicationPublic(Request $illuminateRequest): JsonResponse ], Response::HTTP_FORBIDDEN); } - $userGroups = UserGroup::withContextIds($submission->getData('contextId'))->get(); - - /** @var GenreDAO $genreDao */ - $genreDao = DAORegistry::getDAO('GenreDAO'); - $genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray(); - return response()->json( - Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map( + Repo::publication()->getSchemaMap($submission, collect(), [])->map( $publication, isPublic: true, ), diff --git a/classes/publication/maps/Schema.php b/classes/publication/maps/Schema.php index 46e9693a8ad..31d2eef2b2d 100644 --- a/classes/publication/maps/Schema.php +++ b/classes/publication/maps/Schema.php @@ -68,9 +68,9 @@ public function map(Publication $item, bool $anonymize = false, bool $isPublic = * * 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 c02aaa25968..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; + } } } From df635b5eb3ed58a820151f1295c7d54b22e74b3e Mon Sep 17 00:00:00 2001 From: MrRob Date: Tue, 23 Sep 2025 16:26:26 +0100 Subject: [PATCH 12/12] simplified locale fetching --- api/v1/navigations/resources/NavigationMenuItemResource.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/v1/navigations/resources/NavigationMenuItemResource.php b/api/v1/navigations/resources/NavigationMenuItemResource.php index e3a2642bbd5..43eb4d23878 100644 --- a/api/v1/navigations/resources/NavigationMenuItemResource.php +++ b/api/v1/navigations/resources/NavigationMenuItemResource.php @@ -28,11 +28,9 @@ class NavigationMenuItemResource extends JsonResource public function toArray(Request $request): array { $context = Application::get()->getRequest()->getContext(); - $supportedLocales = $context ? $context->getSupportedLocales() : []; - $titleData = []; if ($this->titleLocaleKey) { - foreach ($supportedLocales as $locale) { + foreach ($context->getSupportedLocales() as $locale) { $titleData[$locale] = Locale::get($this->titleLocaleKey, [], $locale); } }