diff --git a/api/v1/reviews/PKPReviewController.php b/api/v1/reviews/PKPReviewController.php index 8b1c21c7003..a709028f14c 100644 --- a/api/v1/reviews/PKPReviewController.php +++ b/api/v1/reviews/PKPReviewController.php @@ -33,6 +33,7 @@ use PKP\security\authorization\SubmissionAccessPolicy; use PKP\security\authorization\UserRolesRequiredPolicy; use PKP\security\Role; +use PKP\submission\reviewer\ReviewerAction; use PKP\submissionFile\SubmissionFile; class PKPReviewController extends PKPBaseController @@ -71,6 +72,17 @@ public function getGroupRoutes(): void Route::get('history/{submissionId}/{reviewRoundId}', $this->getHistory(...)) ->name('review.get.submission.round.history') ->whereNumber(['reviewRoundId', 'submissionId']); + + Route::put('{submissionId}/{reviewAssignmentId}/confirmReview', $this->confirmReview(...)) + ->name('review.confirm') + ->whereNumber(['reviewAssignmentId', 'submissionId']) + ->middleware([ + self::roleAuthorizer([ + Role::ROLE_ID_SITE_ADMIN, + Role::ROLE_ID_MANAGER, + Role::ROLE_ID_SUB_EDITOR, + ]) + ]); } /** @@ -80,7 +92,6 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme { $this->addPolicy(new UserRolesRequiredPolicy($request), true); $this->addPolicy(new ContextAccessPolicy($request, $roleAssignments)); - $this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments, 'submissionId', true)); return parent::authorize($request, $args, $roleAssignments); @@ -225,4 +236,47 @@ public function getHistory(Request $illuminateRequest): JsonResponse return response()->json($reviewRoundHistory, Response::HTTP_OK); } + + /** + * Accept or decline a review invitation on behalf of a reviewer + */ + public function confirmReview(Request $illuminateRequest): JsonResponse + { + $submissionId = $illuminateRequest->route('submissionId'); + $reviewAssignmentId = $illuminateRequest->route('reviewAssignmentId'); + $acceptReview = $illuminateRequest->decision; + $reviewAssignment = Repo::reviewAssignment()->get($reviewAssignmentId); + + if (!$reviewAssignment) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + $reviewer = Repo::user()->get($reviewAssignment->getReviewerId()); + + if (!isset($reviewer)) { + return response()->json([ + 'error' => __('api.404.resourceNotFound'), + ], Response::HTTP_NOT_FOUND); + } + + if ($acceptReview === 'accept') { + $decline = false; + } elseif ($acceptReview === 'decline') { + $decline = true; + } else { + return response()->json([ + 'error' => __('api.review.assignments.invalidInvitationResponse'), + ], Response::HTTP_BAD_REQUEST); + } + + $submission = Repo::submission()->get($submissionId); + $request = $this->getRequest(); + $reviewerAction = new ReviewerAction(); + $reviewerAction->confirmReview($request, $reviewAssignment, $submission, $decline); + + return response()->json($reviewAssignment, Response::HTTP_OK); + + } } diff --git a/classes/components/forms/decision/LogReviewerResponseForm.php b/classes/components/forms/decision/LogReviewerResponseForm.php new file mode 100644 index 00000000000..7ad68d7d6c7 --- /dev/null +++ b/classes/components/forms/decision/LogReviewerResponseForm.php @@ -0,0 +1,60 @@ +action = $action; + $this->locales = $locales; + $this->addField(new FieldRadioInput('decision', [ + 'groupId' => 'default', + 'label' => __('editor.review.logResponse.form.detail'), + 'options' => [ + [ + 'value' => 'accept', + 'label' => __('editor.review.logResponse.form.option.accepted'), + ], + [ + 'value' => 'decline', + 'label' => __('editor.review.logResponse.form.option.declined'), + ], + ], + 'value' => '', + 'type' => 'radio', + 'isRequired' => true, + 'description' => __('editor.review.logResponse.form.subDetail'), + ]))->addGroup([ + 'id' => 'default', + 'pageId' => 'default', + ])->addPage([ + 'id' => 'default', + 'submitButton' => ['label' => __('editor.review.logResponse')] + ]); + } +} diff --git a/classes/controllers/grid/users/reviewer/PKPReviewerGridHandler.php b/classes/controllers/grid/users/reviewer/PKPReviewerGridHandler.php index 5bffcaa653b..cffd486ccff 100644 --- a/classes/controllers/grid/users/reviewer/PKPReviewerGridHandler.php +++ b/classes/controllers/grid/users/reviewer/PKPReviewerGridHandler.php @@ -61,6 +61,7 @@ use PKP\security\Role; use PKP\security\Validation; use PKP\submission\reviewAssignment\ReviewAssignment; +use PKP\submission\reviewer\ReviewerAction; use PKP\submission\reviewRound\ReviewRound; use PKP\submission\reviewRound\ReviewRoundDAO; use PKP\submission\SubmissionCommentDAO; @@ -1126,7 +1127,8 @@ public function _getReviewAssignmentOps() 'unconsiderReview', 'editReview', 'updateReview', - 'gossip' + 'gossip', + 'addLog' ]; } @@ -1171,6 +1173,7 @@ protected function _getAuthorDeniedOps() 'resendRequestReviewer', 'updateResendRequestReviewer', 'unconsiderReview', 'editReview', 'updateReview', + 'addLog' ]; } @@ -1217,6 +1220,33 @@ protected function createMail(Mailable $mailable, string $emailBody, EmailTempla trigger_error($e->getMessage(), E_USER_WARNING); } } + + /** + * Add the log event + * + * @param $args array + * @param $request PKPRequest + * + * @return JSONMessage JSON object + */ + public function addLog($args, $request) + { + $acceptReview = (bool) $request->getUserVar('acceptReview'); + $decline = !boolval($acceptReview); + + $reviewAssignment = Repo::reviewAssignment()->get($request->getUserVar('reviewAssignmentId')); + $submission = Repo::submission()->get($request->getUserVar('submissionId')); + + $reviewer = Repo::user()->get($reviewAssignment->getReviewerId()); + if (!isset($reviewer)) { + return new JSONMessage(false); + } + + $reviewerAction = new ReviewerAction(); + $reviewerAction->confirmReview($request, $reviewAssignment, $submission, $decline); + + return DAO::getDataChangedEvent($reviewAssignment->getId()); + } } if (!PKP_STRICT_MODE) { diff --git a/controllers/grid/users/reviewer/ReviewerGridRow.php b/controllers/grid/users/reviewer/ReviewerGridRow.php index 2e7c36e711e..b7454b6bc7d 100644 --- a/controllers/grid/users/reviewer/ReviewerGridRow.php +++ b/controllers/grid/users/reviewer/ReviewerGridRow.php @@ -17,11 +17,16 @@ namespace PKP\controllers\grid\users\reviewer; use APP\facades\Repo; +use PKP\components\forms\decision\LogReviewerResponseForm; use PKP\controllers\grid\GridRow; use PKP\core\PKPApplication; +use PKP\db\DAORegistry; use PKP\linkAction\LinkAction; use PKP\linkAction\request\AjaxModal; use PKP\linkAction\request\RedirectConfirmationModal; +use PKP\linkAction\request\VueModal; +use PKP\security\Role; +use PKP\security\RoleDAO; use PKP\security\Validation; use PKP\submission\reviewAssignment\ReviewAssignment; @@ -215,6 +220,30 @@ public function initialize($request, $template = null) ) ); } + + $roleDao = DAORegistry::getDAO('RoleDAO'); /** @var RoleDAO $roleDao */ + $context = $request->getContext(); + + if ($roleDao->userHasRole($context->getId(), $user->getId(), [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER]) && $reviewAssignment->getDateConfirmed() == null) { + $reviewAssignmentId = $reviewAssignment->getId(); + $action = $request->getDispatcher()->url($request, PKPApplication::ROUTE_API, $context->getPath(), "reviews/$submissionId/$reviewAssignmentId/confirmReview"); + $logResponseForm = new LogReviewerResponseForm($action, $context->getSupportedFormLocales(), $context); + $form = [ + 'description' => $submission->getCurrentPublication()->getLocalizedTitle(null, 'html'), + 'logResponseForm' => $logResponseForm->getConfig() + ]; + + $this->addAction( + new LinkAction( + 'logResponse', + new VueModal( + 'WorkflowLogResponseForModal', + array_merge($actionArgs, $form) + ), + __('editor.review.logResponse') + ) + ); + } } } } diff --git a/locale/en/api.po b/locale/en/api.po index bf05f2c3a4c..87d4e456276 100644 --- a/locale/en/api.po +++ b/locale/en/api.po @@ -332,6 +332,9 @@ msgstr "The submission for this review assignment could not be found." msgid "api.reviews.assignments.invalidReviewer" msgstr "The reviewer for the assignment could not be found" +msgid "api.review.assignments.invalidInvitationResponse" +msgstr "Only 'accept' and 'decline' are valid values" + msgid "api.submission.400.sectionDoesNotExist" msgstr "The provided section does not exist." diff --git a/locale/en/editor.po b/locale/en/editor.po index bb71149fe06..d80dd7bd04b 100644 --- a/locale/en/editor.po +++ b/locale/en/editor.po @@ -627,3 +627,24 @@ msgstr "Skip this email" msgid "editor.decision.stepError" msgstr "There was a problem with the {$stepName} step." + +msgid "editor.review.logResponse.for" +msgstr "Log Response for" + +msgid "editor.review.logResponse" +msgstr "Log Response" + +msgid "editor.review.logResponse.form.responseRequired" +msgstr "A response must be selected" + +msgid "editor.review.logResponse.form.option.accepted" +msgstr "Reviewer has accepted the invitation to review" + +msgid "editor.review.logResponse.form.option.declined" +msgstr "Reviewer has declined the invitation to review" + +msgid "editor.review.logResponse.form.detail" +msgstr "Record the response on behalf of the reviewer" + +msgid "editor.review.logResponse.form.subDetail" +msgstr "If the reviewer contacts you through email or any other means, you can record their response for them"