From b4f06b4f4774ecafe0e6ff745fa16cc59fe0c638 Mon Sep 17 00:00:00 2001 From: Nicolas Boulay Date: Wed, 17 Jan 2024 08:47:02 -0500 Subject: [PATCH] pkp/pkp-lib#9453 Let reviewers view their recommendations for previous rounds --- classes/log/SubmissionEmailLogDAO.php | 29 ++++ .../review/ReviewRoundModalHandler.php | 145 ++++++++++++++++++ .../linkAction/ReviewRoundModalLinkAction.php | 87 +++++++++++ locale/en/reviewer.po | 18 +++ locale/fr_CA/reviewer.po | 18 +++ pages/reviewer/PKPReviewerHandler.php | 63 ++++++++ .../modals/reviewRound/reviewRound.tpl | 89 +++++++++++ templates/reviewer/review/reviewRoundTab.tpl | 21 +++ .../reviewer/review/reviewStepHeader.tpl | 4 +- 9 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 controllers/review/ReviewRoundModalHandler.php create mode 100644 controllers/review/linkAction/ReviewRoundModalLinkAction.php create mode 100644 templates/controllers/modals/reviewRound/reviewRound.tpl create mode 100644 templates/reviewer/review/reviewRoundTab.tpl diff --git a/classes/log/SubmissionEmailLogDAO.php b/classes/log/SubmissionEmailLogDAO.php index 88cb18c9f88..5a00269e49e 100644 --- a/classes/log/SubmissionEmailLogDAO.php +++ b/classes/log/SubmissionEmailLogDAO.php @@ -66,6 +66,35 @@ public function getBySubmissionId($submissionId) return $this->getByAssoc(Application::ASSOC_TYPE_SUBMISSION, $submissionId); } + /** + * Get submission email log entries by submission ID, event type and sender ID + * + * @param int $submissionId + * @param int $eventType SubmissionEmailLogEntry::SUBMISSION_EMAIL_* + * @param int $senderId Return only emails sent by this user. + * + * @return DAOResultFactory + */ + function getBySenderId($submissionId, $eventType, $senderId) { + $result = $this->retrieveRange( + 'SELECT e.* + FROM email_log e + WHERE + e.assoc_type = ? AND + e.assoc_id = ? AND + e.event_type = ? AND + e.sender_id = ?', + [ + Application::ASSOC_TYPE_SUBMISSION, + (int) $submissionId, + (int) $eventType, + (int) $senderId + ] + ); + + return new DAOResultFactory($result, $this, 'build'); + } + /** * Create a log entry from data in a Mailable class * diff --git a/controllers/review/ReviewRoundModalHandler.php b/controllers/review/ReviewRoundModalHandler.php new file mode 100644 index 00000000000..dfdd4ff4c3a --- /dev/null +++ b/controllers/review/ReviewRoundModalHandler.php @@ -0,0 +1,145 @@ +addRoleAssignment( + [Role::ROLE_ID_REVIEWER], + ['viewRoundInfo', 'closeModal'] + ); + } + + // + // Implement template methods from PKPHandler. + // + + /** + * @copydoc PKPHandler::authorize() + */ + function authorize($request, &$args, $roleAssignments): bool + { + $this->addPolicy(new RoleBasedHandlerOperationPolicy( + $request, + [Role::ROLE_ID_REVIEWER], + ['viewRoundInfo', 'close'] + )); + + return parent::authorize($request, $args, $roleAssignments); + } + + // + // Public operations + // + + /** + * Display the review round info modal. + * + * @param array $args + * @param PKPRequest $request + * + * @return JSONMessage JSON object + * @throws Exception + */ + function viewRoundInfo($args, $request) + { + $this->setupTemplate($request); + + $submission = Repo::submission()->get($args['submissionId']); + $submissionId = $submission->getId(); + $reviewerId = $request->getUser()->getId(); + + $reviewAssignments = Repo::reviewAssignment()->getCollector() + ->filterByReviewerIds([$reviewerId]) + ->getMany(); + $declinedReviewAssignments = array(); + foreach ($reviewAssignments as $submissionReviewAssignment) { + if ($submissionReviewAssignment->getDeclined() and $submissionId == $submissionReviewAssignment->getSubmissionId()) { + $declinedReviewAssignments[] = $submissionReviewAssignment; + } + } + + $reviewAssignment = Repo::reviewAssignment()->getCollector() + ->filterByReviewRoundIds([$args['reviewRoundId']]) + ->filterByReviewerIds([$reviewerId]) + ->filterByContextIds([$request->getContext()->getId()]) + ->getMany() + ->first(); + $submissionCommentDao = DAORegistry::getDAO('SubmissionCommentDAO'); + $reviewComments = $submissionCommentDao->getReviewerCommentsByReviewerId($submissionId, $reviewerId, $reviewAssignment->getId()); + + $reviewRoundNumber = $args['reviewRoundNumber']; + $submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO'); + $emailLogs = $submissionEmailLogDao + ->getBySenderId($submissionId, SubmissionEmailLogEntry::SUBMISSION_EMAIL_REVIEW_DECLINE, $reviewerId) + ->toArray(); + $declineEmail = null; + $i = 0; + foreach ($declinedReviewAssignments as $declinedReviewAssignment) { + if (isset($emailLogs[$i]) && $reviewRoundNumber == $declinedReviewAssignment->getRound()) { + $declineEmail = $emailLogs[$i]; + } + $i++; + } + + $displayFilesGrid = true; + $lastReviewAssignment = Repo::reviewAssignment()->getCollector() + ->filterBySubmissionIds([$submissionId]) + ->filterByReviewerIds([$reviewerId]) + ->filterByLastReviewRound(true) + ->getMany() + ->first(); + if($lastReviewAssignment->getDeclined() == 1) { + $displayFilesGrid = false; + } + + $templateMgr = TemplateManager::getManager($request); + $templateMgr->assign([ + 'submission' => $submission, + 'reviewAssignment' => $reviewAssignment, + 'reviewRoundNumber' => $reviewRoundNumber, + 'reviewRoundId' => $args['reviewRoundId'], + 'reviewComments' => $reviewComments, + 'declineEmail' => $declineEmail, + 'displayFilesGrid' => $displayFilesGrid + ]); + + return $templateMgr->fetchJson('controllers/modals/reviewRound/reviewRound.tpl'); + } +} diff --git a/controllers/review/linkAction/ReviewRoundModalLinkAction.php b/controllers/review/linkAction/ReviewRoundModalLinkAction.php new file mode 100644 index 00000000000..a956b6adc5e --- /dev/null +++ b/controllers/review/linkAction/ReviewRoundModalLinkAction.php @@ -0,0 +1,87 @@ +_round = $reviewRoundNumber; + + $submission = Repo::submission()->get($submissionId); + $submissionTitle = $submission->getCurrentPublication()->getLocalizedTitle(); + $router = $request->getRouter(); + $actionArgs = [ + 'submissionId' => $submissionId, + 'reviewRoundId' => $reviewRoundId, + 'reviewRoundNumber' => $reviewRoundNumber + ]; + + $ajaxModal = new AjaxModal( + $router->getDispatcher()->url( + $request, + PKPApplication::ROUTE_COMPONENT, + null, + 'review.ReviewRoundModalHandler', + 'viewRoundInfo', + null, + $actionArgs + ), + __( + 'reviewer.submission.reviewRound.info.modal.title', + [ + 'reviewRoundNumber' => $reviewRoundNumber, + 'submissionTitle' => $submissionTitle + ] + ), + 'modal_information' + ); + + // Configure the link action. + parent::__construct('viewRoundInfo', $ajaxModal); + } + + /** + * Get the review round number. + * + * @return int + */ + function getRound(): int + { + return $this->_round; + } +} diff --git a/locale/en/reviewer.po b/locale/en/reviewer.po index c177bf17c8e..af12f341a96 100644 --- a/locale/en/reviewer.po +++ b/locale/en/reviewer.po @@ -94,6 +94,24 @@ msgstr "" "Upload files you would like the editor and/or author to consult, including " "revised versions of the original review file(s)." +msgid "reviewer.submission.reviewRound.info" +msgstr "Read my former reviews: " + +msgid "reviewer.submission.reviewRound.info.modal.title" +msgstr "Evaluation cycle {$reviewRoundNumber} : {$submissionTitle}" + +msgid "reviewer.submission.reviewRound.info.history" +msgstr "Review history" + +msgid "reviewer.submission.comments.authorAndEditor" +msgstr "Author and editor" + +msgid "reviewer.submission.comments.editorOnly" +msgstr "Pour la rédaction seulement" + +msgid "reviewer.submission.comments.review" +msgstr "Review" + msgid "reviewer.complete" msgstr "Review Submitted" diff --git a/locale/fr_CA/reviewer.po b/locale/fr_CA/reviewer.po index fab1b5f7561..a3d335b7355 100644 --- a/locale/fr_CA/reviewer.po +++ b/locale/fr_CA/reviewer.po @@ -107,6 +107,24 @@ msgstr "" "consulter, y compris les versions révisées des fichiers d'évaluation " "originaux." +msgid "reviewer.submission.reviewRound.info" +msgstr "Consulter mes évaluations précédentes : " + +msgid "reviewer.submission.reviewRound.info.modal.title" +msgstr "Évaluation cycle {$reviewRoundNumber} : {$submissionTitle}" + +msgid "reviewer.submission.reviewRound.info.history" +msgstr "Historique d'évaluation" + +msgid "reviewer.submission.comments.authorAndEditor" +msgstr "Pour l'auteur et la rédaction" + +msgid "reviewer.submission.comments.editorOnly" +msgstr "Editor only" + +msgid "reviewer.submission.comments.review" +msgstr "Évaluation" + msgid "reviewer.complete" msgstr "Évaluation envoyée" diff --git a/pages/reviewer/PKPReviewerHandler.php b/pages/reviewer/PKPReviewerHandler.php index da24bfe8ce9..0c1b33a9411 100644 --- a/pages/reviewer/PKPReviewerHandler.php +++ b/pages/reviewer/PKPReviewerHandler.php @@ -24,11 +24,14 @@ use Exception; use Illuminate\Support\Facades\Mail; use PKP\config\Config; +use PKP\controllers\review\linkAction\ReviewRoundModalLinkAction; use PKP\core\JSONMessage; use PKP\core\PKPApplication; use PKP\core\PKPRequest; +use PKP\db\DAORegistry; use PKP\facades\Locale; use PKP\notification\PKPNotification; +use PKP\security\Role; use PKP\submission\reviewAssignment\ReviewAssignment; use PKP\submission\reviewer\form\PKPReviewerReviewStep3Form; use PKP\submission\reviewer\form\ReviewerReviewForm; @@ -41,12 +44,15 @@ class PKPReviewerHandler extends Handler /** * Display the submission review page. + * @throws Exception */ public function submission(array $args, PKPRequest $request): void { $reviewAssignment = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_REVIEW_ASSIGNMENT); /** @var ReviewAssignment $reviewAssignment */ $reviewSubmission = Repo::submission()->get($reviewAssignment->getSubmissionId()); + $reviewSubmissionId = $reviewSubmission->getId(); + $this->insertNewStageAssignmentIfEmpty($request, $reviewSubmissionId); $this->setupTemplate($request); $templateMgr = TemplateManager::getManager($request); @@ -59,11 +65,39 @@ public function submission(array $args, PKPRequest $request): void if ($step < 1 || $step > 4) { throw new Exception('Invalid step!'); } + + $reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); + $reviewRounds = $reviewRoundDao->getBySubmissionId($reviewSubmissionId)->toArray(); + $reviewerId = $reviewAssignment->getReviewerId(); + $reviewRoundsWhereReviewerAssigned = []; + foreach ($reviewRounds as $reviewRound) { + $reviewAssignment = Repo::reviewAssignment()->getCollector() + ->filterByReviewRoundIds([$reviewRound->getId()]) + ->filterByReviewerIds([$reviewerId]) + ->filterByContextIds([$request->getContext()->getId()]) + ->getMany() + ->first(); + if (!is_null($reviewAssignment)) { + $reviewRoundsWhereReviewerAssigned[$reviewRound->getRound()] = $reviewRound; + } + } + + $lastReviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($reviewSubmissionId); + $lastReviewRoundNumber = $lastReviewRound->getRound(); + $reviewRoundHistories = []; + foreach ($reviewRoundsWhereReviewerAssigned as $reviewRound) { + $round = $reviewRound->getRound(); + if ($round != $lastReviewRoundNumber) { + $reviewRoundHistories[$round - 1] = new ReviewRoundModalLinkAction($request, $reviewSubmissionId, $reviewRound->getId(), $round); + } + } + $templateMgr->assign([ 'pageTitle' => __('semicolon', ['label' => __('submission.review')]) . $reviewSubmission->getLocalizedTitle(), 'reviewStep' => $reviewStep, 'selected' => $step - 1, 'submission' => $reviewSubmission, + 'reviewRoundHistories' => $reviewRoundHistories, ]); $templateMgr->setState([ @@ -91,6 +125,7 @@ public function submission(array $args, PKPRequest $request): void /** * Display a step tab contents in the submission review page. + * @throws Exception */ public function step(array $args, PKPRequest $request): JSONMessage { @@ -99,7 +134,9 @@ public function step(array $args, PKPRequest $request): JSONMessage assert(!empty($reviewId)); $reviewSubmission = Repo::submission()->get($reviewAssignment->getSubmissionId()); + $reviewSubmissionId = $reviewSubmission->getId(); + $this->insertNewStageAssignmentIfEmpty($request, $reviewSubmissionId); $this->setupTemplate($request); $reviewStep = max($reviewAssignment->getStep(), 1); // Get the current saved step from the DB @@ -246,4 +283,30 @@ public function _retrieveStep(): int assert(!empty($reviewId)); return $reviewId; } + + /** + * Insert a new stage assignment object if it doesn't already exist. + * + * @param PKPRequest $request + * @param int $reviewSubmissionId + * + * @throws Exception + */ + private function insertNewStageAssignmentIfEmpty(PKPRequest $request, int $reviewSubmissionId): void + { + $reviewerUserGroups = Repo::userGroup() + ->getByRoleIds([Role::ROLE_ID_REVIEWER], $request->getContext()->getId(), true) + ->first(); + $reviewerUserGroupsId = $reviewerUserGroups->getId(); + $userId = $request->getUser()->getId(); + $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); + $result = $stageAssignmentDao->getBySubmissionAndStageId($reviewSubmissionId, null, $reviewerUserGroupsId, $userId); + if (count($result->toArray()) === 0) { + $stageAssignment = $stageAssignmentDao->newDataObject(); + $stageAssignment->setSubmissionId($reviewSubmissionId); + $stageAssignment->setUserId($userId); + $stageAssignment->setUserGroupId($reviewerUserGroupsId); + $stageAssignmentDao->insertObject($stageAssignment); + } + } } diff --git a/templates/controllers/modals/reviewRound/reviewRound.tpl b/templates/controllers/modals/reviewRound/reviewRound.tpl new file mode 100644 index 00000000000..2d884c39bb7 --- /dev/null +++ b/templates/controllers/modals/reviewRound/reviewRound.tpl @@ -0,0 +1,89 @@ +{** + * templates/controllers/modals/reviewRound/reviewRound.tpl + * + * Copyright (c) 2014-2021 Simon Fraser University + * Copyright (c) 2003-2021 John Willinsky + * Copyright (c) 2021 Université Laval + * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. + * + * Display reviewer review round info modal. + *} + + + +
+ +
+ {if $reviewAssignment->getDeclined() == false} +

{translate key="reviewer.article.recommendation"}:

+
+

{$reviewAssignment->getLocalizedRecommendation()}

+
+ + {if !$reviewComments->wasEmpty()} +

{translate key="reviewer.submission.comments.review"}:

+ {iterate from=reviewComments item=reviewComment} +
+ {if $reviewComment->getViewable() == 1} + {translate key="reviewer.submission.comments.authorAndEditor"}: + {else} + {translate key="reviewer.submission.comments.editorOnly"}: + {/if} + {$reviewComment->getComments()} +
+ {/iterate} + {/if} + + {if $displayFilesGrid} + {capture assign="reviewAttachmentsModalUrl"}{url router=$smarty.const.ROUTE_COMPONENT component="grid.files.attachment.ReviewerReviewAttachmentsGridHandler" op="fetchGrid" assocType=$smarty.const.ASSOC_TYPE_REVIEW_ASSIGNMENT assocId=$reviewAssignment->getId() submissionId=$submission->getId() stageId=$reviewAssignment->getStageId() reviewIsClosed=true escape=false}{/capture} + {load_url_in_div id="reviewAttachmentsModal" url=$reviewAttachmentsModalUrl} + {/if} + +
+

{translate key="reviewer.submission.reviewRequestDate"}: {$reviewAssignment->getDateNotified()|date_format:$dateFormatShort}

+

{translate key="reviewer.submission.responseDueDate"}: {$reviewAssignment->getDateResponseDue()|date_format:$dateFormatShort}

+

{translate key="reviewer.submission.reviewDueDate"}: {$reviewAssignment->getDateDue()|date_format:$dateFormatShort}

+

{translate key="common.dateCompleted"}: {$reviewAssignment->getDateCompleted()|date_format:$dateFormatShort}

+
+ + {if $displayFilesGrid} + {capture assign="reviewFilesModalUrl"}{url router=$smarty.const.ROUTE_COMPONENT component="grid.files.review.ReviewerReviewFilesGridHandler" op="fetchGrid" submissionId=$submission->getId() stageId=$reviewAssignment->getStageId() reviewRoundId=$reviewRoundId reviewAssignmentId=$reviewAssignment->getId() escape=false}{/capture} + {load_url_in_div id="reviewFilesModal" url=$reviewFilesModalUrl} + {/if} + {else} +

{translate key="reviewer.submission.reviewDeclineDate"}:

+
+

{$reviewAssignment->getDateConfirmed()|date_format:$dateFormatShort}

+
+

{translate key="reviewer.submission.emailLog"}:

+ {if isset($declineEmail)} +
+

{$declineEmail->getSubject()}
+ {$declineEmail->getBody()} +

+
+ {else} +

{translate key="reviewer.submission.emailLog.defaultMessage"}

+ {/if} + {/if} +
+
+ {fbvFormSection class="formButtons form_buttons"} + {assign var=cancelButtonId value="cancelFormButton"|concat:"-"|uniqid} + {translate key="common.ok"} + + {/fbvFormSection} +
+
diff --git a/templates/reviewer/review/reviewRoundTab.tpl b/templates/reviewer/review/reviewRoundTab.tpl new file mode 100644 index 00000000000..a5d3d08b26f --- /dev/null +++ b/templates/reviewer/review/reviewRoundTab.tpl @@ -0,0 +1,21 @@ +{** + * templates/reviewer/review/reviewRoundTab.tpl + * + * Copyright (c) 2014-2021 Simon Fraser University + * Copyright (c) 2003-2021 John Willinsky + * Copyright (c) 2021 Université Laval + * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. + * + * Build reviewer review round info buttons + *} + +{if $reviewRoundHistories|@count > 0} +
+ {translate key="reviewer.submission.reviewRound.info"}  + {foreach from=$reviewRoundHistories item=reviewRoundHistory key=key} + {assign var=resumeButtonId value="resumeButton"|concat:"-"|uniqid} + {include file="linkAction/buttonGenericLinkAction.tpl" buttonSelector="#"|concat:$resumeButtonId action=$reviewRoundHistory} + {translate key="submission.round" round=$reviewRoundHistory->getRound()} + {/foreach} +
+{/if} diff --git a/templates/reviewer/review/reviewStepHeader.tpl b/templates/reviewer/review/reviewStepHeader.tpl index 389f11850c2..bdb00c120bc 100644 --- a/templates/reviewer/review/reviewStepHeader.tpl +++ b/templates/reviewer/review/reviewStepHeader.tpl @@ -16,7 +16,7 @@ - + + {include file="reviewer/review/reviewRoundTab.tpl"} +