first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
<?php
/**
* @file classes/decision/Collector.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Collector
*
* @brief A helper class to configure a Query Builder to get a collection of editor decisions
*/
namespace PKP\decision;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\interfaces\CollectorInterface;
use PKP\plugins\Hook;
/**
* @template T of Decision
*/
class Collector implements CollectorInterface
{
public DAO $dao;
public ?array $decisionTypes = null;
public ?array $editorIds = null;
public ?array $reviewRoundIds = null;
public ?array $rounds = null;
public ?array $stageIds = null;
public ?array $submissionIds = null;
public function __construct(DAO $dao)
{
$this->dao = $dao;
}
public function getCount(): int
{
return $this->dao->getCount($this);
}
/**
* @return Collection<int,int>
*/
public function getIds(): Collection
{
return $this->dao->getIds($this);
}
/**
* @copydoc DAO::getMany()
* @return LazyCollection<int,T>
*/
public function getMany(): LazyCollection
{
return $this->dao->getMany($this);
}
/**
* Filter decisions by these decision types
*
* @param int[]|null $decisionTypes One of the Decision::* constants
*/
public function filterByDecisionTypes(?array $decisionTypes): self
{
$this->decisionTypes = $decisionTypes;
return $this;
}
/**
* Filter decisions taken by one or more editors]
*
* @param int[]|null $editorIds
*/
public function filterByEditorIds(?array $editorIds): self
{
$this->editorIds = $editorIds;
return $this;
}
/**
* Filter decisions taken in one or more reviewRoundIds
*
* @param int[]|null $reviewRoundIds The review round number, such as first or
* second round of reviews. NOT the unique review round id.
*/
public function filterByReviewRoundIds(?array $reviewRoundIds): self
{
$this->reviewRoundIds = $reviewRoundIds;
return $this;
}
/**
* Filter decisions taken in one or more rounds
*
* @param int[]|null $rounds The review round number, such as first or
* second round of reviews. NOT the unique review round id.
*/
public function filterByRounds(?array $rounds): self
{
$this->rounds = $rounds;
return $this;
}
/**
* Filter decisions taken in one or more workflow stages
*
* @param int[]|null $stageIds One or more WORKFLOW_STAGE_ID_ constants
*/
public function filterByStageIds(?array $stageIds): self
{
$this->stageIds = $stageIds;
return $this;
}
/**
* Filter decisions taken for one or more submission ids
*
* @param int[]|null $submissionIds
*/
public function filterBySubmissionIds(?array $submissionIds): self
{
$this->submissionIds = $submissionIds;
return $this;
}
/**
* @copydoc CollectorInterface::getQueryBuilder()
*/
public function getQueryBuilder(): Builder
{
$qb = DB::table($this->dao->table)
->select([$this->dao->table . '.*'])
->when(!is_null($this->decisionTypes), function ($q) {
$q->whereIn('decision', $this->decisionTypes);
})
->when(!is_null($this->editorIds), function ($q) {
$q->whereIn('editor_id', $this->editorIds);
})
->when(!is_null($this->reviewRoundIds), function ($q) {
$q->whereIn('review_round_id', $this->reviewRoundIds);
})
->when(!is_null($this->rounds), function ($q) {
$q->whereIn('round', $this->rounds);
})
->when(!is_null($this->stageIds), function ($q) {
$q->whereIn('stage_id', $this->stageIds);
})
->when(!is_null($this->submissionIds), function ($q) {
$q->whereIn('submission_id', $this->submissionIds);
})
->orderBy('date_decided', 'asc');
Hook::call('Decision::Collector', [&$qb, $this]);
return $qb;
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
/**
* @file classes/decision/DAO.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @brief Read and write decisions to the database.
*/
namespace PKP\decision;
use APP\decision\Decision;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\EntityDAO;
use PKP\core\traits\EntityWithParent;
/**
* @template T of Decision
* @extends EntityDAO<T>
*/
class DAO extends EntityDAO
{
use EntityWithParent;
/** @copydoc EntityDAO::$schema */
public $schema = \PKP\services\PKPSchemaService::SCHEMA_DECISION;
/** @copydoc EntityDAO::$table */
public $table = 'edit_decisions';
/** @copydoc EntityDAO::$settingsTable */
public $settingsTable = '';
/** @copydoc EntityDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'edit_decision_id';
/** @copydoc EntityDAO::$primaryTableColumns */
public $primaryTableColumns = [
'id' => 'edit_decision_id',
'dateDecided' => 'date_decided',
'decision' => 'decision',
'editorId' => 'editor_id',
'reviewRoundId' => 'review_round_id',
'round' => 'round',
'stageId' => 'stage_id',
'submissionId' => 'submission_id',
];
/**
* Get the parent object ID column name
*/
public function getParentColumn(): string
{
return 'submission_id';
}
/**
* Instantiate a new DataObject
*/
public function newDataObject(): Decision
{
return App::make(Decision::class);
}
/**
* Get the number of decisions matching the configured query
*/
public function getCount(Collector $query): int
{
return $query
->getQueryBuilder()
->count();
}
/**
* Get a list of ids matching the configured query
*
* @return Collection<int,int>
*/
public function getIds(Collector $query): Collection
{
return $query
->getQueryBuilder()
->select($this->table . '.' . $this->primaryKeyColumn)
->pluck($this->table . '.' . $this->primaryKeyColumn);
}
/**
* Get a collection of decisions matching the configured query
* @return LazyCollection<int,T>
*/
public function getMany(Collector $query): LazyCollection
{
$rows = $query
->getQueryBuilder()
->get();
return LazyCollection::make(function () use ($rows) {
foreach ($rows as $row) {
yield $row->edit_decision_id => $this->fromRow($row);
}
});
}
/**
* @copydoc EntityDAO::fromRow()
*/
public function fromRow(object $row): Decision
{
return parent::fromRow($row);
}
/**
* @copydoc EntityDAO::insert()
*/
public function insert(Decision $decision): int
{
return parent::_insert($decision);
}
/**
* @copydoc EntityDAO::update()
*/
public function update(Decision $decision)
{
parent::_update($decision);
}
/**
* @copydoc EntityDAO::delete()
*/
public function delete(Decision $decision)
{
parent::_delete($decision);
}
/**
* Reassign all decisions from one editor to another
*/
public function reassignDecisions(int $fromEditorId, int $toEditorId)
{
DB::table($this->table)
->where('editor_id', '=', $fromEditorId)
->update(['editor_id' => $toEditorId]);
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @defgroup decision Decision
*/
/**
* @file classes/decision/Decision.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Decision
*
* @ingroup decision
*
* @see DAO
*
* @brief An editorial decision taken on a submission, such as to accept, decline or request revisions.
*/
namespace PKP\decision;
use APP\facades\Repo;
use Exception;
use PKP\core\DataObject;
class Decision extends DataObject
{
public const INTERNAL_REVIEW = 1;
public const ACCEPT = 2;
public const EXTERNAL_REVIEW = 3;
public const PENDING_REVISIONS = 4;
public const RESUBMIT = 5;
public const DECLINE = 6;
public const SEND_TO_PRODUCTION = 7;
public const INITIAL_DECLINE = 8;
public const RECOMMEND_ACCEPT = 9;
public const RECOMMEND_PENDING_REVISIONS = 10;
public const RECOMMEND_RESUBMIT = 11;
public const RECOMMEND_DECLINE = 12;
public const RECOMMEND_EXTERNAL_REVIEW = 13; // OMP Specific
public const NEW_EXTERNAL_ROUND = 14;
public const REVERT_DECLINE = 15;
public const REVERT_INITIAL_DECLINE = 16;
public const SKIP_EXTERNAL_REVIEW = 17;
public const SKIP_INTERNAL_REVIEW = 18; // OMP Specific
public const ACCEPT_INTERNAL = 19; // OMP Specific
public const PENDING_REVISIONS_INTERNAL = 20; // OMP Specific
public const RESUBMIT_INTERNAL = 21; // OMP Specific
public const DECLINE_INTERNAL = 22; // OMP Specific
public const RECOMMEND_ACCEPT_INTERNAL = 23; // OMP Specific
public const RECOMMEND_PENDING_REVISIONS_INTERNAL = 24; // OMP Specific
public const RECOMMEND_RESUBMIT_INTERNAL = 25; // OMP Specific
public const RECOMMEND_DECLINE_INTERNAL = 26; // OMP Specific
public const REVERT_INTERNAL_DECLINE = 27; // OMP Specific
public const NEW_INTERNAL_ROUND = 28; // OMP Specific
public const BACK_FROM_PRODUCTION = 29;
public const BACK_FROM_COPYEDITING = 30;
public const CANCEL_REVIEW_ROUND = 31;
public const CANCEL_INTERNAL_REVIEW_ROUND = 32; // OMP Specific
/**
* Get the decision type for this decision
*/
public function getDecisionType(): DecisionType
{
$decisionType = Repo::decision()->getDecisionType($this->getData('decision'));
if (!$decisionType) {
throw new Exception('Decision exists with an unknown type. Decision: ' . $this->getData('decisions'));
}
return $decisionType;
}
}
if (!PKP_STRICT_MODE) {
// Some constants are not redefined here because they never existed as global constants
define('SUBMISSION_EDITOR_DECISION_INITIAL_DECLINE', Decision::INITIAL_DECLINE);
define('SUBMISSION_EDITOR_RECOMMEND_ACCEPT', Decision::RECOMMEND_ACCEPT);
define('SUBMISSION_EDITOR_RECOMMEND_PENDING_REVISIONS', Decision::RECOMMEND_PENDING_REVISIONS);
define('SUBMISSION_EDITOR_RECOMMEND_RESUBMIT', Decision::RECOMMEND_RESUBMIT);
define('SUBMISSION_EDITOR_RECOMMEND_DECLINE', Decision::RECOMMEND_DECLINE);
define('SUBMISSION_EDITOR_DECISION_REVERT_DECLINE', Decision::REVERT_DECLINE);
define('SUBMISSION_EDITOR_DECISION_SEND_TO_PRODUCTION', Decision::SEND_TO_PRODUCTION);
define('SUBMISSION_EDITOR_DECISION_NEW_ROUND', Decision::NEW_EXTERNAL_ROUND);
}
+554
View File
@@ -0,0 +1,554 @@
<?php
/**
* @file classes/decision/DecisionType.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief An interface to define an editorial decision type.
*/
namespace PKP\decision;
use APP\core\Application;
use APP\core\Request;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Exception;
use Illuminate\Support\Facades\App;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\context\LibraryFileDAO;
use PKP\core\Core;
use PKP\db\DAORegistry;
use PKP\file\TemporaryFileManager;
use PKP\mail\EmailData;
use PKP\mail\Mailable;
use PKP\notification\NotificationDAO;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\GenreDAO;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\user\User;
use PKP\validation\ValidatorFactory;
abstract class DecisionType
{
public const ACTION_NOTIFY_AUTHORS = 'notifyAuthors';
public const ACTION_NOTIFY_REVIEWERS = 'notifyReviewers';
public const ACTION_PAYMENT = 'payment';
public const ACTION_DISCUSSION = 'discussion';
public const REVIEW_ASSIGNMENT_COMPLETED = 1;
public const REVIEW_ASSIGNMENT_ACTIVE = 2;
public const REVIEW_ASSIGNMENT_CONFIRMED = 3;
/**
* Get a label/title when this decision has been completed
*
* eg - Submission Accepted
*/
abstract public function getCompletedLabel(): string;
/**
* Get a message for the user to confirm that this decision has been completed
*
* eg - The submission, {$title}, was accepted and sent to the copyediting stage.
*/
abstract public function getCompletedMessage(Submission $submission): string;
/**
* Get the decision type identifier
*
* One of the Decision::* constants
*/
abstract public function getDecision(): int;
/**
* Get a localized description of this decision
*/
abstract public function getDescription(?string $locale = null): string;
/**
* Get a localized label for this decision, such as Accept Submission
*/
abstract public function getLabel(?string $locale = null): string;
/**
* Get the locale key to use for the log entry when this decision is taken
*/
abstract public function getLog(): string;
/**
* Get the status that should be assigned to the last review round when this
* decision is taken.
*
* This will be used by decisions taken in a review stage. If the value is
* null the review round status will be recalculated after the decision is
* recorded.
*/
abstract public function getNewReviewRoundStatus(): ?int;
/**
* Get the status that should be assigned to the submission when this decision is taken.
*
* Null if the status should not be changed.
*/
abstract public function getNewStatus(): ?int;
/**
* Get the workflow stage a submission should be promoted to, if the decision should
* result in moving the submission to another stage
*/
abstract public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int;
/**
* The decision can only be taken when the submission is in this workflow stage
*/
abstract public function getStageId(): int;
/**
* Get a url to record this decision for a submission
*
* @throws Exception If the editorial decision is in the review stage but no review round id has been passed
*/
public function getUrl(Request $request, Context $context, Submission $submission, int $reviewRoundId = null): string
{
$args = [
'decision' => $this->getDecision(),
];
if ($this->isInReview()) {
if (!$reviewRoundId) {
throw new Exception('Can not get URL to the ' . get_class($this) . ' decision without a review round id.');
}
$args['reviewRoundId'] = $reviewRoundId;
}
return $request->getDispatcher()->url(
$request,
Application::ROUTE_PAGE,
$context->getPath(),
'decision',
'record',
$submission->getId(),
$args
);
}
/**
* Is this decision in a review workflow stage?
*/
public function isInReview(): bool
{
return in_array(
$this->getStageId(),
[
WORKFLOW_STAGE_ID_INTERNAL_REVIEW,
WORKFLOW_STAGE_ID_EXTERNAL_REVIEW
]
);
}
/**
* Validate this decision
*
* The default decision properties will already be validated. Use
* this method to validate data for this decision's actions, or
* to apply any additional restrictions for this decision.
*/
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// No validation checks are performed by default
}
/**
* Run actions required to process a new editorial decision
*
* This callback method is fired whenever a new decision is
* recorded. This method changes a submission's status and
* stage id, as well as the review round status.
*
* Add this method to child decision types to perform additional
* actions when the decision is recorded, such as sending emails,
* creating a new review round, etc.
*
* Each decision type can support its own $actions. See the
* decision type class to understand which $actions are
* supported by that type.
*
* Typically, $actions represent a form that was completed or an
* email that was composed while recording the decision.
*
* However, custom decisions are not constrained to these types
* and may use the $actions array to configure any steps
* necessary to record the decision.
*
* The $actions array is a parameter in the REST API so that any
* actions may be sent along with the request to record a decision.
*
* @see Repository::add()
*
* @param array $actions Actions handled by the decision type
*/
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
if ($this->getNewStatus()) {
Repo::submission()->updateStatus($submission, $this->getNewStatus());
}
$newStageId = $this->getNewStageId($submission, (int)$decision->getData('reviewRoundId'));
if ($newStageId) {
$submission->setData('stageId', $newStageId);
Repo::submission()->dao->update($submission);
// Create a new review round if there is not an existing round
// when promoting to a review stage, or reset the review round
// status if one already exists
if (in_array($newStageId, [WORKFLOW_STAGE_ID_INTERNAL_REVIEW, WORKFLOW_STAGE_ID_EXTERNAL_REVIEW])) {
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $newStageId);
if (!is_a($reviewRound, ReviewRound::class)) {
$this->createReviewRound($submission, $newStageId, 1);
} else {
$reviewRoundDao->updateStatus($reviewRound, null);
}
}
}
// Change review round status when a decision is taken in a review stage
if ($reviewRoundId = $decision->getData('reviewRoundId')) {
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getById($reviewRoundId);
if (is_a($reviewRound, ReviewRound::class)) {
// If the decision type doesn't specify a review round status, recalculate
// it from scratch. In order to do this, we unset the ReviewRound's status
// so the DAO will determine the new status
if (is_null($this->getNewReviewRoundStatus())) {
$reviewRound->setData('status', null);
}
$reviewRoundDao->updateStatus($reviewRound, $this->getNewReviewRoundStatus());
}
}
}
/**
* Get the workflow steps for this decision type
*
* Returns null if this decision type does not use a workflow.
* In such cases the decision can be recorded but does not make
* use of the built-in UI for making the decision
*/
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): ?Steps
{
return null;
}
/**
* Get the assigned authors
*/
protected function getAssignedAuthorIds(Submission $submission): array
{
$userIds = [];
/** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
$result = $stageAssignmentDao->getBySubmissionAndRoleIds($submission->getId(), [Role::ROLE_ID_AUTHOR], $this->getStageId());
/** @var StageAssignment $stageAssignment */
while ($stageAssignment = $result->next()) {
$userIds[] = (int) $stageAssignment->getUserId();
}
return $userIds;
}
/**
* Validate the properties of an email action
*
* @return array Empty if no errors
*/
protected function validateEmailAction(array $emailAction, Submission $submission, array $allowedAttachmentFileStages = []): array
{
$schema = (object) [
'attachments' => (object) [
'type' => 'array',
'items' => (object) [
'type' => 'object',
],
],
'bcc' => (object) [
'type' => 'array',
'items' => (object) [
'type' => 'string',
'validation' => [
'email_or_localhost',
],
],
],
'body' => (object) [
'type' => 'string',
'validation' => [
'required',
],
],
'cc' => (object) [
'type' => 'array',
'items' => (object) [
'type' => 'string',
'validation' => [
'email_or_localhost',
],
],
],
'id' => (object) [
'type' => 'string',
'validation' => [
'alpha',
'required',
],
],
'subject' => (object) [
'type' => 'string',
'validation' => [
'required',
],
],
'recipients' => (object) [
'type' => 'array',
'items' => (object) [
'type' => 'integer',
],
],
];
$schemaService = App::make(PKPSchemaService::class);
$rules = [];
foreach ($schema as $propName => $propSchema) {
$rules = $schemaService->addPropValidationRules($rules, $propName, $propSchema);
}
$validator = ValidatorFactory::make(
$emailAction,
$rules,
);
if (isset($emailAction['attachments'])) {
$validator->after(function ($validator) use ($emailAction, $submission, $allowedAttachmentFileStages) {
if ($validator->errors()->get('attachments')) {
return;
}
foreach ($emailAction['attachments'] as $attachment) {
$errorMessage = __('email.attachmentNotFound', ['fileName' => $attachment['name'] ?? '']);
if (isset($attachment['temporaryFileId'])) {
$uploaderId = Application::get()->getRequest()->getUser()->getId();
if (!$this->validateTemporaryFileAttachment($attachment['temporaryFileId'], $uploaderId)) {
$validator->errors()->add('attachments', $errorMessage);
}
} elseif (isset($attachment['submissionFileId'])) {
if (!$this->validateSubmissionFileAttachment((int) $attachment['submissionFileId'], $submission, $allowedAttachmentFileStages)) {
$validator->errors()->add('attachments', $errorMessage);
}
} elseif (isset($attachment['libraryFileId'])) {
if (!$this->validateLibraryAttachment($attachment['libraryFileId'], $submission)) {
$validator->errors()->add('attachments', $errorMessage);
}
} else {
$validator->errors()->add('attachments', $errorMessage);
}
}
});
}
$errors = [];
if ($validator->fails()) {
$errors = $schemaService->formatValidationErrors($validator->errors());
}
return $errors;
}
/**
* Validate a file attachment that has been uploaded by the user
*/
protected function validateTemporaryFileAttachment(string $temporaryFileId, int $uploaderId): bool
{
$temporaryFileManager = new TemporaryFileManager();
return (bool) $temporaryFileManager->getFile($temporaryFileId, $uploaderId);
}
/**
* Validate a file attachment from a submission file
*
* @param array<int> $allowedFileStages SubmissionFile::SUBMISSION_FILE_*
*/
protected function validateSubmissionFileAttachment(int $submissionFileId, Submission $submission, array $allowedFileStages): bool
{
$submissionFile = Repo::submissionFile()->get($submissionFileId);
return $submissionFile
&& $submissionFile->getData('submissionId') === $submission->getId()
&& in_array($submissionFile->getData('fileStage'), $allowedFileStages);
}
/**
* Validate a file attachment from a library file
*/
protected function validateLibraryAttachment(int $libraryFileId, Submission $submission): bool
{
/** @var LibraryFileDAO $libraryFileDao */
$libraryFileDao = DAORegistry::getDAO('LibraryFileDAO');
$file = $libraryFileDao->getById($libraryFileId, $submission->getData('contextId'));
if (!$file) {
return false;
}
return !$file->getSubmissionId() || $file->getSubmissionId() === $submission->getId();
}
/**
* Set an error message for invalid recipients
*
* @param array<int> $invalidRecipientIds
*/
protected function setRecipientError(string $actionErrorKey, array $invalidRecipientIds, Validator $validator)
{
$names = array_map(function ($userId) {
$user = Repo::user()->get((int) $userId);
return $user ? $user->getFullName() : $userId;
}, $invalidRecipientIds);
$validator->errors()->add(
$actionErrorKey . '.to',
__(
'editor.submission.workflowDecision.invalidRecipients',
['names' => join(__('common.commaListSeparator'), $names)]
)
);
}
/**
* Create a fake decision object as if a decision of this
* type was recorded
*
* This decision object can be passed to a Mailable in order to
* prepare data for email templates. The decision is not saved
* to the database and has no `id` property.
*/
protected function getFakeDecision(Submission $submission, User $editor, ?ReviewRound $reviewRound = null): Decision
{
return Repo::decision()->newDataObject([
'dateDecided' => Core::getCurrentDate(),
'decision' => $this->getDecision(),
'editorId' => $editor->getId(),
'reviewRoundId' => $reviewRound ? $reviewRound->getId() : null,
'round' => $reviewRound ? $reviewRound->getRound() : null,
'stageId' => $this->getStageId(),
'submissionId' => $submission->getId(),
]);
}
/**
* Convert a decision action to EmailData
*/
protected function getEmailDataFromAction(array $action): EmailData
{
return new EmailData($action);
}
/**
* Get a Mailable from a decision's action data
*
* Sets the sender, subject, body and attachments.
*
* Does NOT set the recipients.
*/
protected function addEmailDataToMailable(Mailable $mailable, User $sender, EmailData $email): Mailable
{
$mailable
->sender($sender)
->bcc($email->bcc)
->cc($email->cc)
->subject($email->subject)
->body($email->body);
if (!empty($email->attachments)) {
foreach ($email->attachments as $attachment) {
if (isset($attachment[Mailable::ATTACHMENT_TEMPORARY_FILE])) {
$mailable->attachTemporaryFile(
$attachment[Mailable::ATTACHMENT_TEMPORARY_FILE],
$attachment['name'],
$sender->getId()
);
} elseif (isset($attachment[Mailable::ATTACHMENT_SUBMISSION_FILE])) {
$mailable->attachSubmissionFile(
$attachment[Mailable::ATTACHMENT_SUBMISSION_FILE],
$attachment['name']
);
} elseif (isset($attachment[Mailable::ATTACHMENT_LIBRARY_FILE])) {
$mailable->attachLibraryFile(
$attachment[Mailable::ATTACHMENT_LIBRARY_FILE],
$attachment['name']
);
}
}
}
return $mailable;
}
/**
* Create a review round in a review stage
*/
protected function createReviewRound(Submission $submission, int $stageId, ?int $round = 1)
{
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->build(
$submission->getId(),
$stageId,
$round,
ReviewRound::REVIEW_ROUND_STATUS_PENDING_REVIEWERS
);
// Create review round status notification
/** @var NotificationDAO $notificationDao */
$notificationDao = DAORegistry::getDAO('NotificationDAO');
$notificationFactory = $notificationDao->getByAssoc(
Application::ASSOC_TYPE_REVIEW_ROUND,
$reviewRound->getId(),
null,
Notification::NOTIFICATION_TYPE_REVIEW_ROUND_STATUS,
$submission->getData('contextId')
);
if (!$notificationFactory->next()) {
$notificationMgr = new NotificationManager();
$notificationMgr->createNotification(
Application::get()->getRequest(),
null,
Notification::NOTIFICATION_TYPE_REVIEW_ROUND_STATUS,
$submission->getData('contextId'),
Application::ASSOC_TYPE_REVIEW_ROUND,
$reviewRound->getId()
);
}
}
/**
* Helper method to get the file genres for a context
*/
protected function getFileGenres(int $contextId): array
{
/** @var GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
return $genreDao->getByContextId($contextId)->toArray();
}
}
+481
View File
@@ -0,0 +1,481 @@
<?php
/**
* @file classes/decision/Repository.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage editorial decisions.
*/
namespace PKP\decision;
use APP\core\Application;
use APP\core\Request;
use APP\core\Services;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use PKP\context\Context;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
use PKP\log\event\PKPSubmissionEventLogEntry;
use PKP\log\SubmissionLog;
use PKP\observers\events\DecisionAdded;
use PKP\plugins\Hook;
use PKP\security\Role;
use PKP\security\Validation;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
use PKP\validation\ValidatorFactory;
abstract class Repository
{
/** @var DAO $dao */
public $dao;
/** @var string $schemaMap The name of the class to map this entity to its schemaa */
public $schemaMap = maps\Schema::class;
/** @var Request $request */
protected $request;
/** @var PKPSchemaService<Decision> $schemaService */
protected $schemaService;
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
$this->dao = $dao;
$this->request = $request;
$this->schemaService = $schemaService;
}
/** @copydoc DAO::newDataObject() */
public function newDataObject(array $params = []): Decision
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::get() */
public function get(int $id, int $submissionId = null): ?Decision
{
return $this->dao->get($id, $submissionId);
}
/** @copydoc DAO::exists() */
public function exists(int $id, int $submissionId = null): bool
{
return $this->dao->exists($id, $submissionId);
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return App::make(Collector::class);
}
/**
* Get an instance of the map class for mapping
* decisions to their schema
*/
public function getSchemaMap(): maps\Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Validate properties for a decision
*
* Perform validation checks on data used to add a decision. It is not
* possible to edit a decision.
*
* @param array $props A key/value array with the new data to validate
* @param Submission $submission The submission for this decision
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate(array $props, DecisionType $decisionType, Submission $submission, Context $context): array
{
// Return early if no valid decision type exists
if (!isset($props['decision']) || $props['decision'] !== $decisionType->getDecision()) {
return ['decision' => [__('editor.submission.workflowDecision.typeInvalid')]];
}
// Return early if an invalid submission ID is passed
if (!isset($props['submissionId']) || $props['submissionId'] !== $submission->getId()) {
return ['submissionId' => [__('editor.submission.workflowDecision.submissionInvalid')]];
}
$validator = ValidatorFactory::make(
$props,
$this->schemaService->getValidationRules($this->dao->schema, []),
);
// Check required
ValidatorFactory::required(
$validator,
null,
$this->schemaService->getRequiredProps($this->dao->schema),
$this->schemaService->getMultilingualProps($this->dao->schema),
[],
''
);
$validator->after(function ($validator) use ($props, $decisionType, $submission, $context) {
// The decision stage id must match the decision type's stage id
// and the submission's current workflow stage
if ($props['stageId'] !== $decisionType->getStageId()
|| $props['stageId'] !== $submission->getData('stageId')) {
$validator->errors()->add('decision', __('editor.submission.workflowDecision.invalidStage'));
}
// The editorId must match an existing editor
if (isset($props['editorId'])) {
$user = Repo::user()->get((int) $props['editorId']);
if (!$user) {
$validator->errors()->add('editorId', __('editor.submission.workflowDecision.invalidEditor'));
}
}
// A recommendation can not be made if the submission does not
// have at least one assigned editor who can make a decision
if ($this->isRecommendation($decisionType->getDecision())) {
/** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
$assignedEditorIds = $stageAssignmentDao->getDecidingEditorIds($submission->getId(), $decisionType->getStageId());
if (!$assignedEditorIds) {
$validator->errors()->add('decision', __('editor.submission.workflowDecision.requiredDecidingEditor'));
}
}
// Validate the review round
if (isset($props['reviewRoundId'])) {
// The decision must be taken during a review stage
if (!$decisionType->isInReview() && !$validator->errors()->get('reviewRoundId')) {
$validator->errors()->add('reviewRoundId', __('editor.submission.workflowDecision.invalidReviewRoundStage'));
}
// The review round must exist and be related to the correct submission.
if (!$validator->errors()->get('reviewRoundId')) {
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getById($props['reviewRoundId']);
if (!$reviewRound) {
$validator->errors()->add('reviewRoundId', __('editor.submission.workflowDecision.invalidReviewRound'));
} elseif ($reviewRound->getSubmissionId() !== $submission->getId()) {
$validator->errors()->add('reviewRoundId', __('editor.submission.workflowDecision.invalidReviewRoundSubmission'));
}
}
} elseif ($decisionType->isInReview()) {
$validator->errors()->add('reviewRoundId', __('editor.submission.workflowDecision.requiredReviewRound'));
}
// Allow the decision type to add validation checks
$decisionType->validate($props, $submission, $context, $validator, isset($reviewRound) ? $reviewRound->getId() : null);
});
$errors = [];
if ($validator->fails()) {
$errors = $this->schemaService->formatValidationErrors($validator->errors());
}
Hook::call('Decision::validate', [&$errors, $props]);
return $errors;
}
/**
* Record an editorial decision
*/
public function add(Decision $decision): int
{
// Actions are handled separately from the decision object
$actions = $decision->getData('actions') ?? [];
$decision->unsetData('actions');
// Set the review round automatically from the review round id
if ($decision->getData('reviewRoundId')) {
$decision->setData('round', $this->getRoundByReviewRoundId($decision->getData('reviewRoundId')));
}
$decision->setData('dateDecided', Core::getCurrentDate());
$id = $this->dao->insert($decision);
Hook::call('Decision::add', [$decision]);
$decision = $this->get($id);
$decisionType = $decision->getDecisionType();
$submission = Repo::submission()->get($decision->getData('submissionId'));
$editor = Repo::user()->get($decision->getData('editorId'));
$decision = $this->get($decision->getId());
$context = Application::get()->getRequest()->getContext();
if (!$context || $context->getId() !== $submission->getData('contextId')) {
$context = Services::get('context')->get($submission->getData('contextId'));
}
// Log the decision
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => $this->isRecommendation($decisionType->getDecision())
? PKPSubmissionEventLogEntry::SUBMISSION_LOG_EDITOR_RECOMMENDATION
: PKPSubmissionEventLogEntry::SUBMISSION_LOG_EDITOR_DECISION,
'userId' => Validation::loggedInAs() ?? $this->request->getUser()?->getId(),
'message' => $decisionType->getLog(),
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate()
]);
Repo::eventLog()->add($eventLog);
// Allow the decision type to perform additional actions
$decisionType->runAdditionalActions($decision, $submission, $editor, $context, $actions);
try {
event(new DecisionAdded(
$decision,
$decisionType,
$submission,
$editor,
$context,
$actions
));
} catch (Exception $e) {
error_log($e->getMessage());
error_log($e->getTraceAsString());
}
$this->updateNotifications($decision, $decisionType, $submission);
return $id;
}
/**
* Delete all decisions by the submission ID
*/
public function deleteBySubmissionId(int $submissionId)
{
$decisionIds = $this->getCollector()
->filterBySubmissionIds([$submissionId])
->getIds();
foreach ($decisionIds as $decisionId) {
$this->dao->deleteById($decisionId);
}
}
/**
* Get a decision type by the DECISION::* constant
*/
public function getDecisionType(int $decision): ?DecisionType
{
$decision = $this->getDecisionTypes()->first(function (DecisionType $decisionType) use ($decision) {
return $decisionType->getDecision() === $decision;
});
return $decision ?? null;
}
/**
* Find the most recent revisions decision that is still active. An active
* decision is one that is not overriden by any other decision.
*/
public function getActivePendingRevisionsDecision(int $submissionId, int $stageId, int $decision = Decision::PENDING_REVISIONS): ?Decision
{
$postReviewDecisions = [Decision::SEND_TO_PRODUCTION];
$revisionDecisions = [Decision::PENDING_REVISIONS, Decision::RESUBMIT];
if (!in_array($decision, $revisionDecisions)) {
return null;
}
$revisionsDecisions = $this->getCollector()
->filterBySubmissionIds([$submissionId])
->getMany();
// Most recent decision first
$revisionsDecisions = $revisionsDecisions->reverse();
$pendingRevisionDecision = null;
foreach ($revisionsDecisions as $revisionDecision) {
if (in_array($revisionDecision->getData('decision'), $postReviewDecisions)) {
// Decisions at later stages do not override the pending revisions one.
continue;
} elseif ($revisionDecision->getData('decision') == $decision) {
if ($revisionDecision->getData('stageId') == $stageId) {
$pendingRevisionDecision = $revisionDecision;
// Only the last pending revisions decision is relevant.
break;
} else {
// Both internal and external pending revisions decisions are
// valid at the same time. Continue to search.
continue;
}
} else {
break;
}
}
return $pendingRevisionDecision;
}
/**
* Have any submission files been uploaded to the revision file stage since
* this decision was taken?
*/
public function revisionsUploadedSinceDecision(Decision $decision, int $submissionId): bool
{
$stageId = $decision->getData('stageId');
$round = $decision->getData('round');
$sentRevisions = false;
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getReviewRound($submissionId, $stageId, $round);
$submissionFiles = Repo::submissionFile()
->getCollector()
->filterByReviewRoundIds([$reviewRound->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION])
->getMany();
foreach ($submissionFiles as $submissionFile) {
if ($submissionFile->getData('updatedAt') > $decision->getData('dateDecided')) {
$sentRevisions = true;
break;
}
}
return $sentRevisions;
}
/**
* Get a list of all the decision types available
*
* @return Collection<int,DecisionType>
*/
abstract public function getDecisionTypes(): Collection;
/**
* Get a list of the decline decision types
*
* @return DecisionType[]
*/
abstract public function getDeclineDecisionTypes(): array;
/**
* Get a list of the decision types that a recommending user is
* allowed to make given a submission stage id.
*
* @return DecisionType[]
*/
abstract public function getDecisionTypesMadeByRecommendingUsers(int $stageId): array;
/**
* Is the given decision a recommendation?
*/
public function isRecommendation(int $decision): bool
{
return in_array($decision, [
Decision::RECOMMEND_ACCEPT,
Decision::RECOMMEND_DECLINE,
Decision::RECOMMEND_PENDING_REVISIONS,
Decision::RECOMMEND_RESUBMIT,
]);
}
protected function getRoundByReviewRoundId(int $reviewRoundId): int
{
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getById($reviewRoundId);
return $reviewRound->getData('round');
}
/**
* Update notifications controlled by the NotificationManager
*/
protected function updateNotifications(Decision $decision, DecisionType $decisionType, Submission $submission)
{
$notificationMgr = new NotificationManager();
// Update editor decision and pending revisions notifications.
$notificationTypes = $this->getReviewNotificationTypes();
if ($editorDecisionNotificationType = $notificationMgr->getNotificationTypeByEditorDecision($decision)) {
array_unshift($notificationTypes, $editorDecisionNotificationType);
}
$authorIds = [];
/** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
$result = $stageAssignmentDao->getBySubmissionAndRoleIds($submission->getId(), [Role::ROLE_ID_AUTHOR], $decisionType->getStageId());
/** @var StageAssignment $stageAssignment */
while ($stageAssignment = $result->next()) {
$authorIds[] = (int) $stageAssignment->getUserId();
}
$notificationMgr->updateNotification(
Application::get()->getRequest(),
$notificationTypes,
$authorIds,
Application::ASSOC_TYPE_SUBMISSION,
$submission->getId()
);
// Update submission notifications
$submissionNotificationTypes = $this->getSubmissionNotificationTypes($decision);
if (count($submissionNotificationTypes)) {
$notificationMgr->updateNotification(
Application::get()->getRequest(),
$submissionNotificationTypes,
null,
Application::ASSOC_TYPE_SUBMISSION,
$submission->getId()
);
}
}
/**
* Get the notification types related to a review stage
*
* @return int[] One or more of the Notification::NOTIFICATION_TYPE_ constants
*/
abstract protected function getReviewNotificationTypes(): array;
/**
* Get additional notifications to be updated on a submission
*
* @return int[] One or more of the Notification::NOTIFICATION_TYPE_ constants
*/
protected function getSubmissionNotificationTypes(Decision $decision): array
{
switch ($decision->getData('decision')) {
case Decision::ACCEPT:
return [
Notification::NOTIFICATION_TYPE_ASSIGN_COPYEDITOR,
Notification::NOTIFICATION_TYPE_AWAITING_COPYEDITS
];
case Decision::SEND_TO_PRODUCTION:
return [
Notification::NOTIFICATION_TYPE_ASSIGN_COPYEDITOR,
Notification::NOTIFICATION_TYPE_AWAITING_COPYEDITS,
Notification::NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER,
Notification::NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS,
];
}
return [];
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
/**
* @file classes/decision/Step.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Step
*
* @brief A base class to define a step in an editorial decision workflow
*/
namespace PKP\decision;
use Exception;
use stdClass;
abstract class Step
{
public string $id;
public string $type;
public string $name;
public string $description;
/**
* @param string $id A unique id for this step
* @param string $name The name of this step. Shown to the user.
* @param string $description A description of this step. Shown to the user.
*/
public function __construct(string $id, string $name, string $description = '')
{
$this->id = $id;
$this->name = $name;
$this->description = $description;
if (!isset($this->type)) {
throw new Exception('Decision workflow step created without specifying a type.');
}
}
/**
* Compile initial state data to pass to the frontend
*/
public function getState(): stdClass
{
$config = new stdClass();
$config->id = $this->id;
$config->type = $this->type;
$config->name = $this->name;
$config->description = $this->description;
$config->errors = new stdClass();
return $config;
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* @file classes/decision/Steps.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Steps
*
* @brief A base class to define a step-by-step workflow to record a decision type
*/
namespace PKP\decision;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class Steps
{
public DecisionType $decisionType;
public Submission $submission;
public Context $context;
public ?ReviewRound $reviewRound;
public array $steps = [];
public function __construct(DecisionType $decisionType, Submission $submission, Context $context, ?ReviewRound $reviewRound = null)
{
$this->decisionType = $decisionType;
$this->submission = $submission;
$this->context = $context;
if ($reviewRound) {
$this->reviewRound = $reviewRound;
}
}
/**
* Add a step to the workflow
*
* @param bool $prepend Pass true to add this step before other steps
*/
public function addStep(Step $step, bool $prepend = false)
{
if ($prepend) {
array_unshift($this->steps, $step);
} else {
$this->steps[$step->id] = $step;
}
}
/**
* Compile initial state data to pass to the frontend
*
* @see DecisionPage.vue
*/
public function getState(): array
{
$state = [];
foreach ($this->steps as $step) {
$state[] = $step->getState();
}
return $state;
}
/**
* Get all users assigned to a role in this decision's stage
*
* @param integer $roleId
*
* @return array<User>
*/
public function getStageParticipants(int $roleId): array
{
/** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
$userIds = [];
$result = $stageAssignmentDao->getBySubmissionAndRoleIds(
$this->submission->getId(),
[$roleId],
$this->decisionType->getStageId()
);
/** @var StageAssignment $stageAssignment */
while ($stageAssignment = $result->next()) {
$userIds[] = (int) $stageAssignment->getUserId();
}
$users = [];
foreach (array_unique($userIds) as $authorUserId) {
$users[] = Repo::user()->get($authorUserId);
}
return $users;
}
/**
* Get all reviewers who completed a review in this decision's stage
*
* @param array<\PKP\submission\reviewAssignment\ReviewAssignment> $reviewAssignments
*
* @return array<User>
*/
public function getReviewersFromAssignments(array $reviewAssignments): array
{
$reviewers = [];
foreach ($reviewAssignments as $reviewAssignment) {
$reviewers[] = Repo::user()->get((int) $reviewAssignment->getReviewerId());
}
return $reviewers;
}
/**
* Get all assigned editors who can make a decision in this stage
*/
public function getDecidingEditors(): array
{
/** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
$userIds = $stageAssignmentDao->getDecidingEditorIds($this->submission->getId(), $this->decisionType->getStageId());
$users = [];
foreach (array_unique($userIds) as $authorUserId) {
$users[] = Repo::user()->get($authorUserId);
}
return $users;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
/**
* @file classes/decision/maps/Schema.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Schema
*
* @brief Map editorial decisions to the properties defined in their schema
*/
namespace PKP\decision\maps;
use APP\decision\Decision;
use APP\facades\Repo;
use Illuminate\Support\Enumerable;
use PKP\services\PKPSchemaService;
class Schema extends \PKP\core\maps\Schema
{
public Enumerable $collection;
public string $schema = PKPSchemaService::SCHEMA_DECISION;
/**
* Map a decision
*
* Includes all properties in the decision schema.
*/
public function map(Decision $item): array
{
return $this->mapByProperties($this->getProps(), $item);
}
/**
* Map a collection of Decisions
*
* @see self::map
*/
public function mapMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) {
return $this->map($item);
});
}
/**
* Map schema properties of a Decision to an assoc array
*/
protected function mapByProperties(array $props, Decision $item): array
{
$type = Repo::decision()->getDecisionType($item->getData('decision'));
$output = [];
foreach ($props as $prop) {
switch ($prop) {
case '_href':
$output[$prop] = $this->getApiUrl('submissions/' . (int) $item->getData('submissionId') . '/decisions/' . (int) $item->getId());
break;
case 'description':
$output[$prop] = $type ? $type->getDescription() : '';
break;
case 'label':
$output[$prop] = $type ? $type->getLabel() : '';
break;
default:
$output[$prop] = $item->getData($prop);
break;
}
}
ksort($output);
return $this->withExtensions($output, $item);
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
/**
* @file classes/decision/steps/Email.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Email
*
* @brief A step in an editorial decision workflow that shows an email composer.
*/
namespace PKP\decision\steps;
use APP\core\Application;
use APP\facades\Repo;
use PKP\components\fileAttachers\BaseAttacher;
use PKP\decision\Step;
use PKP\emailTemplate\EmailTemplate;
use PKP\facades\Locale;
use PKP\mail\Mailable;
use PKP\user\User;
use stdClass;
class Email extends Step
{
/** @var array<BaseAttacher> */
public array $attachers;
public bool $canChangeRecipients = false;
public bool $canSkip = true;
public bool $anonymousRecipients = false;
public array $locales;
public Mailable $mailable;
/** @var array<User> */
public array $recipients;
public string $type = 'email';
/**
* @param array<User> $recipients One or more User objects who are the recipients of this email
* @param Mailable $mailable The mailable that will be used to send this email
* @param array<BaseAttacher> $attachers
*/
public function __construct(string $id, string $name, string $description, array $recipients, Mailable $mailable, array $locales, ?array $attachers = [])
{
parent::__construct($id, $name, $description);
$this->attachers = $attachers;
$this->locales = $locales;
$this->mailable = $mailable;
$this->recipients = $recipients;
}
/**
* Can the editor change the recipients of this email
*/
public function canChangeRecipients(bool $value): self
{
$this->canChangeRecipients = $value;
return $this;
}
/**
* Should the recipient names be shown in the
* email body and subject when writing the email
*/
public function anonymizeRecipients(bool $value): self
{
$this->anonymousRecipients = $value;
return $this;
}
/**
* Can the editor skip this email
*/
public function canSkip(bool $value): self
{
$this->canSkip = $value;
return $this;
}
public function getState(): stdClass
{
$config = parent::getState();
$config->attachers = $this->getAttachers();
$config->canChangeRecipients = $this->canChangeRecipients;
$config->canSkip = $this->canSkip;
$config->emailTemplates = $this->getEmailTemplates();
$config->initialTemplateKey = $this->mailable::getEmailTemplateKey();
$config->recipientOptions = $this->getRecipientOptions();
$config->anonymousRecipients = $this->anonymousRecipients;
$config->variables = [];
$config->locale = Locale::getLocale();
$config->locales = [];
foreach ($this->locales as $locale) {
$config->variables[$locale] = $this->getVariables($locale);
$config->locales[] = [
'locale' => $locale,
'name' => Locale::getMetadata($locale)->getDisplayName(),
];
}
return $config;
}
protected function getRecipientOptions(): array
{
$recipientOptions = [];
foreach ($this->recipients as $user) {
$names = [];
foreach ($this->locales as $locale) {
$names[$locale] = $user->getFullName(true, false, $locale);
}
$recipientOptions[] = [
'value' => $user->getId(),
'label' => $names,
];
}
return $recipientOptions;
}
protected function getEmailTemplates(): array
{
$request = Application::get()->getRequest();
$context = $request->getContext();
$emailTemplates = collect();
if ($this->mailable::getEmailTemplateKey()) {
$emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $this->mailable::getEmailTemplateKey());
if ($emailTemplate) {
$emailTemplates->add($emailTemplate);
}
Repo::emailTemplate()
->getCollector($context->getId())
->alternateTo([$this->mailable::getEmailTemplateKey()])
->getMany()
->each(fn (EmailTemplate $e) => $emailTemplates->add($e));
}
return Repo::emailTemplate()->getSchemaMap()->mapMany($emailTemplates)->toArray();
}
protected function getAttachers(): array
{
$attachers = [];
foreach ($this->attachers as $attacher) {
$attachers[] = $attacher->getState();
}
return $attachers;
}
/**
* Format the mailable variables into an array to
* pass to the Composer component
*/
protected function getVariables(string $locale): array
{
$data = $this->mailable->getData($locale);
$descriptions = $this->mailable::getDataDescriptions();
$variables = [];
foreach ($data as $key => $value) {
$variables[] = [
'key' => $key,
'value' => $value,
'description' => $descriptions[$key] ?? '',
];
}
return $variables;
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* @file classes/decision/steps/Form.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Form
*
* @brief A step in an editorial decision workflow that shows a form to be completed
*/
namespace PKP\decision\steps;
use PKP\components\forms\FormComponent;
use PKP\decision\Step;
use stdClass;
class Form extends Step
{
public string $type = 'form';
public FormComponent $form;
/**
* @param FormComponent $form The form to show in this step
*/
public function __construct(string $id, string $name, string $description, FormComponent $form)
{
parent::__construct($id, $name, $description);
$this->form = $form;
}
public function getState(): stdClass
{
$config = parent::getState();
$config->form = $this->form->getConfig();
// Decision forms shouldn't have submit buttons
// because the step-by-step decision wizard includes
// next/previous buttons
unset($config->form['pages'][0]['submitButton']);
return $config;
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @file classes/decision/steps/PromoteFiles.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PromoteFiles
*
* @brief A step in an editorial decision workflow that allows the editor to
* copy files from one or more file stages to a new stage.
*/
namespace PKP\decision\steps;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\decision\Step;
use PKP\submission\Genre;
use PKP\submissionFile\Collector;
use stdClass;
class PromoteFiles extends Step
{
public string $type = 'promoteFiles';
public int $to;
public array $lists = [];
/** @var int[] */
public array $selected = [];
public Submission $submission;
/** @var Genre[] $genres File genres in this context */
public array $genres = [];
/**
* @param integer $to Selected files are copied to this file stage
* @param Genre[] $genres File genres in this context
*/
public function __construct(string $id, string $name, string $description, int $to, Submission $submission, array $genres)
{
parent::__construct($id, $name, $description);
$this->submission = $submission;
$this->to = $to;
$this->genres = $genres;
}
/**
* Add a list of files that can be copied to the next stage
*
* @param bool $selectedByDefault Whether the files in this list should be selected by default
*/
public function addFileList(string $name, Collector $collector, bool $selectedByDefault = true): self
{
$files = $collector->getMany();
$fileSummaries = Repo::submissionFile()
->getSchemaMap()
->summarizeMany($files, $this->genres);
$this->lists[] = [
'name' => $name,
'files' => $fileSummaries->values(),
];
if ($selectedByDefault && $files->count()) {
$this->selected = array_merge(
$this->selected,
$files->map(fn ($file) => $file->getId())->all()
);
}
return $this;
}
public function getState(): stdClass
{
$config = parent::getState();
$config->to = $this->to;
$config->selected = $this->selected;
$config->lists = $this->lists;
return $config;
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
/**
* @file classes/decision/types/Accept.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Accept
*
* @brief A decision to accept a submission for publication.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\steps\PromoteFiles;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\decision\types\traits\NotifyReviewers;
use PKP\mail\mailables\DecisionAcceptNotifyAuthor;
use PKP\mail\mailables\DecisionNotifyReviewer;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class Accept extends DecisionType
{
use InExternalReviewRound;
use NotifyAuthors;
use NotifyReviewers;
public function getDecision(): int
{
return Decision::ACCEPT;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): int
{
return WORKFLOW_STAGE_ID_EDITING;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return ReviewRound::REVIEW_ROUND_STATUS_ACCEPTED;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.accept', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.accept.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.accept.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.accept.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.accept.completedDescription', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->validateNotifyReviewersAction($action, $actionErrorKey, $validator, $submission, $reviewRoundId, self::REVIEW_ASSIGNMENT_COMPLETED);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $decision->getData('reviewRoundId'), self::REVIEW_ASSIGNMENT_COMPLETED);
$emailData = $this->getEmailDataFromAction($action);
$this->sendAuthorEmail(
new DecisionAcceptNotifyAuthor($context, $submission, $decision, $reviewAssignments),
$emailData,
$editor,
$submission,
$context
);
$this->shareReviewAttachmentFiles($emailData->attachments, $submission, $decision->getData('reviewRoundId'));
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->sendReviewersEmail(
new DecisionNotifyReviewer($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor, $reviewRound);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRound->getId(), self::REVIEW_ASSIGNMENT_COMPLETED);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionAcceptNotifyAuthor($context, $submission, $fakeDecision, $reviewAssignments);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.accept.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
if (count($reviewAssignments)) {
$reviewers = $steps->getReviewersFromAssignments($reviewAssignments);
$mailable = new DecisionNotifyReviewer($context, $submission, $fakeDecision);
$steps->addStep(
(new Email(
$this->ACTION_NOTIFY_REVIEWERS,
__('editor.submission.decision.notifyReviewers'),
__('editor.submission.decision.notifyReviewers.description'),
$reviewers,
$mailable->sender($editor),
$context->getSupportedFormLocales(),
$fileAttachers
))
->canChangeRecipients(true)
->anonymizeRecipients(true)
);
}
$steps->addStep((new PromoteFiles(
'promoteFilesToCopyediting',
__('editor.submission.selectFiles'),
__('editor.submission.decision.promoteFiles.copyediting'),
SubmissionFile::SUBMISSION_FILE_FINAL,
$submission,
$this->getFileGenres($context->getId())
))->addFileList(
__('editor.submission.revisions'),
Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION])
->filterByReviewRoundIds([$reviewRound->getId()])
));
return $steps;
}
}
@@ -0,0 +1,232 @@
<?php
/**
* @file classes/decision/types/BackFromCopyediting.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class BackFromCopyediting
*
* @brief A decision to return a submission back from the copyediting stage. It follows as
* if has any external review round, back to external review stage.
* if has any internal review round but no external review sound, back to internal review stage.
* if has no internal and external review round, backt to submission stage.
*
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\components\fileAttachers\FileStage;
use PKP\components\fileAttachers\Library;
use PKP\components\fileAttachers\Upload;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionBackFromCopyeditingNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class BackFromCopyediting extends DecisionType
{
use NotifyAuthors;
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getDecision(): int
{
return Decision::BACK_FROM_COPYEDITING;
}
public function getStageId(): int
{
return WORKFLOW_STAGE_ID_EDITING;
}
/**
* Determine the possible new stage id for this decision
*
* The determining process follows as :
*
* If there is any external review round associated with it,
* new stage need to be external review stage
*
* If there is no external review round associated with it but there is internal review round,
* new stage need to be internal review stage
*
* If there is no external or internal review round associated with it
* new stage need to submission stage
*/
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
if ($reviewRoundDao->submissionHasReviewRound($submission->getId(), WORKFLOW_STAGE_ID_EXTERNAL_REVIEW)) {
return WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
}
if ($reviewRoundDao->submissionHasReviewRound($submission->getId(), WORKFLOW_STAGE_ID_INTERNAL_REVIEW)) {
return WORKFLOW_STAGE_ID_INTERNAL_REVIEW;
}
return WORKFLOW_STAGE_ID_SUBMISSION;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.backFromCopyediting', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.backFromCopyediting.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.backFromCopyediting.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.backFromCopyediting.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.backFromCopyediting.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
if($reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId())) {
$reviewRoundDao->updateStatus($reviewRound, ReviewRound::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW);
}
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionBackFromCopyeditingNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionBackFromCopyeditingNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.backFromCopyediting.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
return $steps;
}
/**
* Get the submission file stages that are permitted to be attached to emails
* sent in this decision
*
* @return int[]
*/
protected function getAllowedAttachmentFileStages(): array
{
return [
SubmissionFile::SUBMISSION_FILE_FINAL,
];
}
/**
* Get the file attacher components supported for emails in this decision
*/
protected function getFileAttachers(Submission $submission, Context $context): array
{
$attachers = [
new Upload(
$context,
__('common.upload.addFile'),
__('common.upload.addFile.description'),
__('common.upload.addFile')
),
];
$attachers[] = (new FileStage(
$context,
$submission,
__('submission.submit.submissionFiles'),
__('email.addAttachment.submissionFiles.submissionDescription'),
__('email.addAttachment.submissionFiles.attach')
))
->withFileStage(
SubmissionFile::SUBMISSION_FILE_FINAL,
__('submission.finalDraft')
);
$attachers[] = new Library(
$context,
$submission
);
return $attachers;
}
}
@@ -0,0 +1,196 @@
<?php
/**
* @file classes/decision/types/BackFromProduction.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class BackFromProduction
*
* @brief A decision to return a submission back from the production stage.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\components\fileAttachers\FileStage;
use PKP\components\fileAttachers\Library;
use PKP\components\fileAttachers\Upload;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionBackFromProductionNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class BackFromProduction extends DecisionType
{
use NotifyAuthors;
public function getDecision(): int
{
return Decision::BACK_FROM_PRODUCTION;
}
public function getStageId(): int
{
return WORKFLOW_STAGE_ID_PRODUCTION;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): int
{
return WORKFLOW_STAGE_ID_EDITING;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.backToCopyediting', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.backToCopyediting.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.backToCopyediting.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.backToCopyediting.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.backToCopyediting.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionBackFromProductionNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionBackFromProductionNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.backToCopyediting.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
return $steps;
}
/**
* Get the submission file stages that are permitted to be attached to emails
* sent in this decision
*
* @return array<int>
*/
protected function getAllowedAttachmentFileStages(): array
{
return [
SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY,
];
}
/**
* Get the file attacher components supported for emails in this decision
*/
protected function getFileAttachers(Submission $submission, Context $context): array
{
$attachers = [
new Upload(
$context,
__('common.upload.addFile'),
__('common.upload.addFile.description'),
__('common.upload.addFile')
),
];
$attachers[] = (new FileStage(
$context,
$submission,
__('submission.submit.submissionFiles'),
__('email.addAttachment.submissionFiles.submissionDescription'),
__('email.addAttachment.submissionFiles.attach')
))
->withFileStage(
SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY,
__('editor.submission.production.productionReadyFiles')
);
$attachers[] = new Library(
$context,
$submission
);
return $attachers;
}
}
@@ -0,0 +1,245 @@
<?php
/**
* @file classes/decision/types/CancelReviewRound.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CancelReviewRound
*
* @brief A decision to cancel a review round and send the submission back
* to the previous review round or workflow stage.
*
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\interfaces\DecisionRetractable;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\decision\types\traits\NotifyReviewers;
use PKP\mail\mailables\DecisionCancelReviewRoundNotifyAuthor;
use PKP\mail\mailables\ReviewerUnassign;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\user\User;
class CancelReviewRound extends DecisionType implements DecisionRetractable
{
use NotifyAuthors;
use NotifyReviewers;
use InExternalReviewRound;
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getDecision(): int
{
return Decision::CANCEL_REVIEW_ROUND;
}
/**
* Determine the possible new stage id for this decision
*
* The determining process follows as :
*
* If there is more than one external review round associated with it
* new stage need to be external review stage
*
* If there is only one external review round associated with it but there is internal review round also associated with it,
* new stage need to be internal review stage
*
* If there is no external or internal review round associated with it
* new stage need to submission stage
*/
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
if ($reviewRoundDao->getReviewRoundCountBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_EXTERNAL_REVIEW) > 1) {
return WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
}
if ($reviewRoundDao->submissionHasReviewRound($submission->getId(), WORKFLOW_STAGE_ID_INTERNAL_REVIEW)) {
return WORKFLOW_STAGE_ID_INTERNAL_REVIEW;
}
return WORKFLOW_STAGE_ID_SUBMISSION;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.cancelReviewRound', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.cancelReviewRound.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.cancelReviewRound.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.cancelReviewRound.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.cancelReviewRound.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!$this->canRetract($submission, $reviewRoundId)) {
$validator->errors()->add('reviewRoundId', __('editor.submission.decision.cancelReviewRound.restriction'));
}
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->validateNotifyReviewersAction($action, $actionErrorKey, $validator, $submission, $reviewRoundId, DecisionType::REVIEW_ASSIGNMENT_ACTIVE);
break;
}
}
}
/**
* Determine if the review round can be cancelled
*
* The determining process follows as:
* If there is any submitted review by reviewer that is not cancelled, can not back out
* If there is any completed review by reviewer, can not back out
*/
public function canRetract(Submission $submission, ?int $reviewRoundId): bool
{
if (!$reviewRoundId) {
return false;
}
$confirmedReviewerIds = $this->getReviewerIds($submission->getId(), $reviewRoundId, self::REVIEW_ASSIGNMENT_CONFIRMED);
if (count($confirmedReviewerIds) > 0) {
return false;
}
$completedReviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRoundId, self::REVIEW_ASSIGNMENT_COMPLETED);
if (count($completedReviewAssignments) > 0) {
return false;
}
return true;
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionCancelReviewRoundNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->sendReviewersEmail(
new ReviewerUnassign($context, $submission, null, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission
);
break;
}
}
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var \PKP\submission\reviewAssignment\ReviewAssignmentDAO $reviewAssignmentDao */
$reviewRoundId = $decision->getData('reviewRoundId');
$reviewAssignmentDao->deleteByReviewRoundId($reviewRoundId);
$reviewRoundDao->deleteById($reviewRoundId);
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): ?Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionCancelReviewRoundNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.cancelReviewRound.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRound->getId(), DecisionType::REVIEW_ASSIGNMENT_ACTIVE);
if (count($reviewAssignments)) {
$reviewers = $steps->getReviewersFromAssignments($reviewAssignments);
$mailable = new ReviewerUnassign($context, $submission, null, $fakeDecision);
$steps->addStep((new Email(
$this->ACTION_NOTIFY_REVIEWERS,
__('editor.submission.decision.notifyReviewers'),
__('editor.submission.decision.reviewerUnassigned.notifyReviewers.description'),
$reviewers,
$mailable->sender($editor),
$context->getSupportedFormLocales(),
$fileAttachers
))->canChangeRecipients(true));
}
return $steps;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
/**
* @file classes/decision/types/Decline.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Decline
*
* @brief A decision to decline a submission for publication.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\decision\types\traits\NotifyReviewers;
use PKP\mail\mailables\DecisionDeclineNotifyAuthor;
use PKP\mail\mailables\DecisionNotifyReviewer;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class Decline extends DecisionType
{
use InExternalReviewRound;
use NotifyAuthors;
use NotifyReviewers;
public function getDecision(): int
{
return Decision::DECLINE;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return Submission::STATUS_DECLINED;
}
public function getNewReviewRoundStatus(): ?int
{
return ReviewRound::REVIEW_ROUND_STATUS_DECLINED;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.decline', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.decline.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.decline.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.decline.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.decline.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->validateNotifyReviewersAction($action, $actionErrorKey, $validator, $submission, $reviewRoundId, self::REVIEW_ASSIGNMENT_COMPLETED);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $decision->getData('reviewRoundId'), self::REVIEW_ASSIGNMENT_COMPLETED);
$emailData = $this->getEmailDataFromAction($action);
$this->sendAuthorEmail(
new DecisionDeclineNotifyAuthor($context, $submission, $decision, $reviewAssignments),
$emailData,
$editor,
$submission,
$context
);
$this->shareReviewAttachmentFiles($emailData->attachments, $submission, $decision->getData('reviewRoundId'));
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->sendReviewersEmail(
new DecisionNotifyReviewer($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor, $reviewRound);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRound->getId(), self::REVIEW_ASSIGNMENT_COMPLETED);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionDeclineNotifyAuthor($context, $submission, $fakeDecision, $reviewAssignments);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.decline.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
if (count($reviewAssignments)) {
$reviewers = $steps->getReviewersFromAssignments($reviewAssignments);
$mailable = new DecisionNotifyReviewer($context, $submission, $fakeDecision);
$steps->addStep(
(new Email(
$this->ACTION_NOTIFY_REVIEWERS,
__('editor.submission.decision.notifyReviewers'),
__('editor.submission.decision.notifyReviewers.description'),
$reviewers,
$mailable->sender($editor),
$context->getSupportedFormLocales(),
$fileAttachers
))
->canChangeRecipients(true)
->anonymizeRecipients(true)
);
}
return $steps;
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @file classes/decision/types/InitialDecline.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class InitialDecline
*
* @brief A decision to decline a submission in the initial submission stage.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\InSubmissionStage;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionInitialDeclineNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class InitialDecline extends DecisionType
{
use InSubmissionStage;
use NotifyAuthors;
public function getDecision(): int
{
return Decision::INITIAL_DECLINE;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): int
{
return Submission::STATUS_DECLINED;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.decline', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.initialDecline.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.decline.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.decline.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.decline.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionInitialDeclineNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionInitialDeclineNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.decline.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
return $steps;
}
}
@@ -0,0 +1,174 @@
<?php
/**
* @file classes/decision/types/NewExternalReviewRound.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class NewExternalReviewRound
*
* @brief A decision to open a new round of review in the external review stage
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\steps\PromoteFiles;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionNewReviewRoundNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class NewExternalReviewRound extends DecisionType
{
use InExternalReviewRound;
use NotifyAuthors;
public function getDecision(): int
{
return Decision::NEW_EXTERNAL_ROUND;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return ReviewRound::REVIEW_ROUND_STATUS_PENDING_REVIEWERS;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.newReviewRound', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.newReviewRound.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.newReviewRound.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.newReviewRound.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.newReviewRound.completedDescription', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
/** @var ReviewRound $reviewRound */
$reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $this->getNewStageId($submission, $decision->getData('reviewRoundId')));
$this->createReviewRound($submission, $this->getStageId(), $reviewRound->getRound() + 1);
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionNewReviewRoundNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor, $reviewRound);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionNewReviewRoundNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.newReviewRound.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
$steps->addStep((new PromoteFiles(
'promoteFilesToReviewRound',
__('editor.submission.selectFiles'),
__('editor.submission.decision.promoteFiles.review'),
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
$submission,
$this->getFileGenres($context->getId())
))->addFileList(
__('editor.submission.revisions'),
Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION])
->filterByReviewRoundIds([$reviewRound->getId()])
));
return $steps;
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/decision/types/RecommendAccept.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RecommendAccept
*
* @brief A recommendation to accept a submission for publication.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use PKP\decision\DecisionType;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\IsRecommendation;
class RecommendAccept extends DecisionType
{
use InExternalReviewRound;
use IsRecommendation;
public function getDecision(): int
{
return Decision::RECOMMEND_ACCEPT;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.recommend.accept', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.recommend.accept.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.recommend.accept.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.recommend.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.recommend.completed.description');
}
public function getRecommendationLabel(): string
{
return __('editor.submission.decision.accept');
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/decision/types/RecommendDecline.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RecommendDecline
*
* @brief A recommendation that the submission be declined.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use PKP\decision\DecisionType;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\IsRecommendation;
class RecommendDecline extends DecisionType
{
use InExternalReviewRound;
use IsRecommendation;
public function getDecision(): int
{
return Decision::RECOMMEND_DECLINE;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.recommend.decline', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.recommend.decline.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.recommend.decline.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.recommend.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.recommend.completed.description');
}
public function getRecommendationLabel(): string
{
return __('editor.submission.decision.decline');
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/decision/types/RecommendResubmit.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RecommendResubmit
*
* @brief A recommendation to request revisions to be sent for another round of review.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use PKP\decision\DecisionType;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\IsRecommendation;
class RecommendResubmit extends DecisionType
{
use InExternalReviewRound;
use IsRecommendation;
public function getDecision(): int
{
return Decision::RECOMMEND_RESUBMIT;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.recommend.resubmit', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.recommend.resubmit.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.recommend.resubmit.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.recommend.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.recommend.completed.description');
}
public function getRecommendationLabel(): string
{
return __('editor.submission.decision.resubmit');
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/decision/types/RecommendRevisions.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RecommendRevisions
*
* @brief A recommendation to request revisions before accepting a submission.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use PKP\decision\DecisionType;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\IsRecommendation;
class RecommendRevisions extends DecisionType
{
use InExternalReviewRound;
use IsRecommendation;
public function getDecision(): int
{
return Decision::RECOMMEND_PENDING_REVISIONS;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.recommend.revisions', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.recommend.revisions.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.recommend.revisions.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.recommend.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.recommend.completed.description');
}
public function getRecommendationLabel(): string
{
return __('editor.submission.decision.requestRevisions');
}
}
@@ -0,0 +1,183 @@
<?php
/**
* @file classes/decision/types/RequestRevisions.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RequestRevisions
*
* @brief A decision to request revisions for a submission.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\decision\types\traits\NotifyReviewers;
use PKP\mail\mailables\DecisionNotifyReviewer;
use PKP\mail\mailables\DecisionRequestRevisionsNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class RequestRevisions extends DecisionType
{
use InExternalReviewRound;
use NotifyAuthors;
use NotifyReviewers;
public function getDecision(): int
{
return Decision::PENDING_REVISIONS;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return ReviewRound::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.requestRevisions', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.requestRevisions.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.requestRevisions.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.requestRevisions.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.requestRevisions.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->validateNotifyReviewersAction($action, $actionErrorKey, $validator, $submission, $reviewRoundId, self::REVIEW_ASSIGNMENT_COMPLETED);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $decision->getData('reviewRoundId'), self::REVIEW_ASSIGNMENT_COMPLETED);
$emailData = $this->getEmailDataFromAction($action);
$this->sendAuthorEmail(
new DecisionRequestRevisionsNotifyAuthor($context, $submission, $decision, $reviewAssignments),
$emailData,
$editor,
$submission,
$context
);
$this->shareReviewAttachmentFiles($emailData->attachments, $submission, $decision->getData('reviewRoundId'));
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->sendReviewersEmail(
new DecisionNotifyReviewer($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor, $reviewRound);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRound->getId(), self::REVIEW_ASSIGNMENT_COMPLETED);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionRequestRevisionsNotifyAuthor($context, $submission, $fakeDecision, $reviewAssignments);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.requestRevisions.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
if (count($reviewAssignments)) {
$reviewers = $steps->getReviewersFromAssignments($reviewAssignments);
$mailable = new DecisionNotifyReviewer($context, $submission, $fakeDecision);
$steps->addStep(
(new Email(
$this->ACTION_NOTIFY_REVIEWERS,
__('editor.submission.decision.notifyReviewers'),
__('editor.submission.decision.notifyReviewers.description'),
$reviewers,
$mailable->sender($editor),
$context->getSupportedFormLocales(),
$fileAttachers
))
->canChangeRecipients(true)
->anonymizeRecipients(true)
);
}
return $steps;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
/**
* @file classes/decision/types/Resubmit.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Resubmit
*
* @brief A decision to request the author submit revisions for another round of review.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\decision\types\traits\NotifyReviewers;
use PKP\mail\mailables\DecisionNotifyReviewer;
use PKP\mail\mailables\DecisionResubmitNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class Resubmit extends DecisionType
{
use InExternalReviewRound;
use NotifyAuthors;
use NotifyReviewers;
public function getDecision(): int
{
return Decision::RESUBMIT;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return ReviewRound::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.resubmit', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.resubmit.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.resubmit.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.resubmit.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.resubmit.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->validateNotifyReviewersAction($action, $actionErrorKey, $validator, $submission, $reviewRoundId, self::REVIEW_ASSIGNMENT_COMPLETED);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $decision->getData('reviewRoundId'), self::REVIEW_ASSIGNMENT_COMPLETED);
$emailData = $this->getEmailDataFromAction($action);
$this->sendAuthorEmail(
new DecisionResubmitNotifyAuthor($context, $submission, $decision, $reviewAssignments),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
$this->shareReviewAttachmentFiles($emailData->attachments, $submission, $decision->getData('reviewRoundId'));
break;
case $this->ACTION_NOTIFY_REVIEWERS:
$this->sendReviewersEmail(
new DecisionNotifyReviewer($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor, $reviewRound);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRound->getId(), self::REVIEW_ASSIGNMENT_COMPLETED);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionResubmitNotifyAuthor($context, $submission, $fakeDecision, $reviewAssignments);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.requestRevisions.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
if (count($reviewAssignments)) {
$reviewers = $steps->getReviewersFromAssignments($reviewAssignments);
$mailable = new DecisionNotifyReviewer($context, $submission, $fakeDecision);
$steps->addStep(
(new Email(
$this->ACTION_NOTIFY_REVIEWERS,
__('editor.submission.decision.notifyReviewers'),
__('editor.submission.decision.notifyReviewers.description'),
$reviewers,
$mailable->sender($editor),
$context->getSupportedFormLocales(),
$fileAttachers
))
->canChangeRecipients(true)
->anonymizeRecipients(true)
);
}
return $steps;
}
}
@@ -0,0 +1,147 @@
<?php
/**
* @file classes/decision/types/RevertDecline.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RevertDecline
*
* @brief A decision to revert a declined submission and return it to the active queue.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\InExternalReviewRound;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionRevertDeclineNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class RevertDecline extends DecisionType
{
use InExternalReviewRound;
use NotifyAuthors;
public function getDecision(): int
{
return Decision::REVERT_DECLINE;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): int
{
return Submission::STATUS_QUEUED;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.revertDecline', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.revertDecline.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.revertDecline.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.revertDecline.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.revertDecline.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
// If there is no review round id, a validation error will already have been set
if (!$reviewRoundId) {
return;
}
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionRevertDeclineNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor, $reviewRound);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionRevertDeclineNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.revertDecline.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
return $steps;
}
}
@@ -0,0 +1,142 @@
<?php
/**
* @file classes/decision/types/RevertInitialDecline.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class RevertInitialDecline
*
* @brief A decision to revert a declined submission and return it to the active queue when it is still in the submission stage.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\types\traits\InSubmissionStage;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionRevertInitialDeclineNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\user\User;
class RevertInitialDecline extends DecisionType
{
use InSubmissionStage;
use NotifyAuthors;
public function getDecision(): int
{
return Decision::REVERT_INITIAL_DECLINE;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): ?int
{
return null;
}
public function getNewStatus(): int
{
return Submission::STATUS_QUEUED;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.revertDecline', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.revertDecline.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.revertDecline.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.revertDecline.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.revertInitialDecline.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionRevertInitialDeclineNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionRevertInitialDeclineNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.revertDecline.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
return $steps;
}
}
@@ -0,0 +1,184 @@
<?php
/**
* @file classes/decision/types/SendExternalReview.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SendExternalReview
*
* @brief A decision to send a submission to the external review stage.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\steps\PromoteFiles;
use PKP\decision\types\traits\InSubmissionStage;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionSendExternalReviewNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class SendExternalReview extends DecisionType
{
use InSubmissionStage;
use NotifyAuthors;
public function getDecision(): int
{
return Decision::EXTERNAL_REVIEW;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): int
{
return WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.sendExternalReview', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.sendExternalReview.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.sendExternalReview.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.sendExternalReview.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.sendExternalReview.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionSendExternalReviewNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionSendExternalReviewNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.sendExternalReview.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
$promoteFilesStep = new PromoteFiles(
'promoteFilesToReview',
__('editor.submission.selectFiles'),
__('editor.submission.decision.promoteFiles.externalReview'),
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
$submission,
$this->getFileGenres($context->getId())
);
$steps->addStep($this->withFilePromotionLists($submission, $promoteFilesStep));
return $steps;
}
/**
* Get the submission file stages that are permitted to be attached to emails
* sent in this decision
*
* @return int[]
*/
protected function getAllowedAttachmentFileStages(): array
{
return [
SubmissionFile::SUBMISSION_FILE_SUBMISSION,
];
}
/**
* Get the file promotion step with file promotion lists
* added to it
*/
protected function withFilePromotionLists(Submission $submission, PromoteFiles $step): PromoteFiles
{
return $step->addFileList(
__('submission.submit.submissionFiles'),
Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_SUBMISSION])
);
}
}
@@ -0,0 +1,225 @@
<?php
/**
* @file classes/decision/types/SendToProduction.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SendToProduction
*
* @brief A decision to send a submission to the production stage.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\components\fileAttachers\FileStage;
use PKP\components\fileAttachers\Library;
use PKP\components\fileAttachers\Upload;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\steps\PromoteFiles;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionSendToProductionNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class SendToProduction extends DecisionType
{
use NotifyAuthors;
public function getDecision(): int
{
return Decision::SEND_TO_PRODUCTION;
}
public function getStageId(): int
{
return WORKFLOW_STAGE_ID_EDITING;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): int
{
return WORKFLOW_STAGE_ID_PRODUCTION;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.sendToProduction', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.sendToProduction.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.sendToProduction.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.sendToProduction.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.sendToProduction.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionSendToProductionNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionSendToProductionNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.sendToProduction.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
$steps->addStep((new PromoteFiles(
'promoteFilesToProduction',
__('editor.submission.selectFiles'),
__('editor.submission.decision.promoteFiles.production'),
SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY,
$submission,
$this->getFileGenres($context->getId())
))->addFileList(
__('submission.copyedited'),
Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_COPYEDIT])
)->addFileList(
__('submission.finalDraft'),
Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_FINAL]),
false
));
return $steps;
}
/**
* Get the submission file stages that are permitted to be attached to emails
* sent in this decision
*
* @return array<int>
*/
protected function getAllowedAttachmentFileStages(): array
{
return [
SubmissionFile::SUBMISSION_FILE_FINAL,
SubmissionFile::SUBMISSION_FILE_COPYEDIT,
];
}
/**
* Get the file attacher components supported for emails in this decision
*/
protected function getFileAttachers(Submission $submission, Context $context): array
{
$attachers = [
new Upload(
$context,
__('common.upload.addFile'),
__('common.upload.addFile.description'),
__('common.upload.addFile')
),
];
$attachers[] = (new FileStage(
$context,
$submission,
__('submission.submit.submissionFiles'),
__('email.addAttachment.submissionFiles.submissionDescription'),
__('email.addAttachment.submissionFiles.attach')
))
->withFileStage(
SubmissionFile::SUBMISSION_FILE_COPYEDIT,
__('submission.copyedited')
)
->withFileStage(
SubmissionFile::SUBMISSION_FILE_FINAL,
__('submission.finalDraft')
);
$attachers[] = new Library(
$context,
$submission
);
return $attachers;
}
}
@@ -0,0 +1,165 @@
<?php
/**
* @file classes/decision/types/SkipExternalReview.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SkipExternalReview
*
* @brief A decision to accept a submission, skip the review stage and send it to the copyediting stage.
*/
namespace PKP\decision\types;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\decision\steps\PromoteFiles;
use PKP\decision\types\traits\InSubmissionStage;
use PKP\decision\types\traits\NotifyAuthors;
use PKP\mail\mailables\DecisionSkipExternalReviewNotifyAuthor;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
class SkipExternalReview extends DecisionType
{
use InSubmissionStage;
use NotifyAuthors;
public function getDecision(): int
{
return Decision::SKIP_EXTERNAL_REVIEW;
}
public function getStageId(): int
{
return WORKFLOW_STAGE_ID_SUBMISSION;
}
public function getNewStageId(Submission $submission, ?int $reviewRoundId): int
{
return WORKFLOW_STAGE_ID_EDITING;
}
public function getNewStatus(): ?int
{
return null;
}
public function getNewReviewRoundStatus(): ?int
{
return null;
}
public function getLabel(?string $locale = null): string
{
return __('editor.submission.decision.skipReview', [], $locale);
}
public function getDescription(?string $locale = null): string
{
return __('editor.submission.decision.skipReview.description', [], $locale);
}
public function getLog(): string
{
return 'editor.submission.decision.skipReview.log';
}
public function getCompletedLabel(): string
{
return __('editor.submission.decision.skipReview.completed');
}
public function getCompletedMessage(Submission $submission): string
{
return __('editor.submission.decision.skipReview.completed.description', ['title' => $submission?->getCurrentPublication()?->getLocalizedFullTitle(null, 'html') ?? '']);
}
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
parent::validate($props, $submission, $context, $validator, $reviewRoundId);
if (!isset($props['actions'])) {
return;
}
foreach ((array) $props['actions'] as $index => $action) {
$actionErrorKey = 'actions.' . $index;
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->validateNotifyAuthorsAction($action, $actionErrorKey, $validator, $submission);
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_NOTIFY_AUTHORS:
$this->sendAuthorEmail(
new DecisionSkipExternalReviewNotifyAuthor($context, $submission, $decision),
$this->getEmailDataFromAction($action),
$editor,
$submission,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context);
$authors = $steps->getStageParticipants(Role::ROLE_ID_AUTHOR);
if (count($authors)) {
$mailable = new DecisionSkipExternalReviewNotifyAuthor($context, $submission, $fakeDecision);
$steps->addStep(new Email(
$this->ACTION_NOTIFY_AUTHORS,
__('editor.submission.decision.notifyAuthors'),
__('editor.submission.decision.skipReview.notifyAuthorsDescription'),
$authors,
$mailable
->sender($editor)
->recipients($authors),
$context->getSupportedFormLocales(),
$fileAttachers
));
}
$steps->addStep((new PromoteFiles(
'promoteFilesToReview',
__('editor.submission.selectFiles'),
__('editor.submission.decision.promoteFiles.copyediting'),
SubmissionFile::SUBMISSION_FILE_FINAL,
$submission,
$this->getFileGenres($context->getId())
))->addFileList(
__('submission.submit.submissionFiles'),
Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_SUBMISSION])
));
return $steps;
}
}
@@ -0,0 +1,27 @@
<?php
/**
* @file classes/decision/types/interfaces/DecisionRetractable.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions to determine if a decision to back out of a review
* round or stage can be recorded
*/
namespace PKP\decision\types\interfaces;
use APP\submission\Submission;
interface DecisionRetractable
{
/**
* Determine if a decision to back out of a review round or stage can be recorded
*
* @return bool Result to decide if decision can be retractable
*/
public function canRetract(Submission $submission, ?int $reviewRoundId): bool;
}
@@ -0,0 +1,126 @@
<?php
/**
* @file classes/decision/types/traits/InExternalReviewRound.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions for decisions taken in an external review round
*/
namespace PKP\decision\types\traits;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\components\fileAttachers\FileStage;
use PKP\components\fileAttachers\Library;
use PKP\components\fileAttachers\ReviewFiles;
use PKP\components\fileAttachers\Upload;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
trait InExternalReviewRound
{
use WithReviewAssignments;
/** @copydoc DecisionType::getStageId() */
public function getStageId(): int
{
return WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
}
/** Helper method so self::getFileAttachers() can be extended for other review stages */
protected function getRevisionFileStage(): int
{
return SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION;
}
/** Helper method so self::getFileAttachers() can be extended for other review stages */
protected function getReviewFileStage(): int
{
return SubmissionFile::SUBMISSION_FILE_REVIEW_FILE;
}
/**
* Get the submission file stages that are permitted to be attached to emails
* sent in this decision
*
* @return array<int>
*/
protected function getAllowedAttachmentFileStages(): array
{
return [
SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT,
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION,
];
}
/**
* Get the file attacher components supported for emails in this decision
*/
protected function getFileAttachers(Submission $submission, Context $context, ?ReviewRound $reviewRound = null): array
{
$attachers = [
new Upload(
$context,
__('common.upload.addFile'),
__('common.upload.addFile.description'),
__('common.upload.addFile')
),
];
if ($reviewRound) {
/** @var ReviewAssignmentDAO $reviewAssignmentDAO */
$reviewAssignmentDAO = DAORegistry::getDAO('ReviewAssignmentDAO');
$reviewAssignments = $reviewAssignmentDAO->getByReviewRoundId($reviewRound->getId());
$reviewerFiles = [];
if (!empty($reviewAssignments)) {
$reviewerFiles = Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByAssoc(Application::ASSOC_TYPE_REVIEW_ASSIGNMENT, array_keys($reviewAssignments))
->getMany();
}
$attachers[] = new ReviewFiles(
__('reviewer.submission.reviewFiles'),
__('email.addAttachment.reviewFiles.description'),
__('email.addAttachment.reviewFiles.attach'),
$reviewerFiles,
$reviewAssignments,
$context
);
}
$attachers[] = (new FileStage(
$context,
$submission,
__('submission.submit.submissionFiles'),
__('email.addAttachment.submissionFiles.reviewDescription'),
__('email.addAttachment.submissionFiles.attach')
))
->withFileStage(
$this->getRevisionFileStage(),
__('editor.submission.revisions'),
$reviewRound
)->withFileStage(
$this->getReviewFileStage(),
__('reviewer.submission.reviewFiles'),
$reviewRound
);
$attachers[] = new Library(
$context,
$submission
);
return $attachers;
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/decision/types/traits/InSubmissionStage.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions for decisions taken in the submission stage
*/
namespace PKP\decision\types\traits;
use APP\submission\Submission;
use PKP\components\fileAttachers\FileStage;
use PKP\components\fileAttachers\Library;
use PKP\components\fileAttachers\Upload;
use PKP\context\Context;
use PKP\submissionFile\SubmissionFile;
trait InSubmissionStage
{
public function getStageId(): int
{
return WORKFLOW_STAGE_ID_SUBMISSION;
}
/**
* Get the submission file stages that are permitted to be attached to emails
* sent in this decision
*
* @return array<int>
*/
protected function getAllowedAttachmentFileStages(): array
{
return [
SubmissionFile::SUBMISSION_FILE_SUBMISSION,
];
}
/**
* Get the file attacher components supported for emails in this decision
*/
protected function getFileAttachers(Submission $submission, Context $context): array
{
$attachers = [
new Upload(
$context,
__('common.upload.addFile'),
__('common.upload.addFile.description'),
__('common.upload.addFile')
),
];
$attachers[] = (new FileStage(
$context,
$submission,
__('submission.submit.submissionFiles'),
__('email.addAttachment.submissionFiles.submissionDescription'),
__('email.addAttachment.submissionFiles.attach')
))
->withFileStage(
SubmissionFile::SUBMISSION_FILE_SUBMISSION,
__('submission.submit.submissionFiles')
);
$attachers[] = new Library(
$context,
$submission
);
return $attachers;
}
}
@@ -0,0 +1,240 @@
<?php
/**
* @file classes/decision/types/traits/IsRecommendation.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions for decisions that are recommendations
*/
namespace PKP\decision\types\traits;
use APP\core\Application;
use APP\core\Services;
use APP\decision\Decision;
use APP\facades\Repo;
use APP\submission\Submission;
use Exception;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\decision\DecisionType;
use PKP\decision\Steps;
use PKP\decision\steps\Email;
use PKP\facades\Locale;
use PKP\file\TemporaryFileManager;
use PKP\mail\EmailData;
use PKP\mail\Mailable;
use PKP\mail\mailables\RecommendationNotifyEditors;
use PKP\note\Note;
use PKP\query\QueryDAO;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
trait IsRecommendation
{
protected string $ACTION_DISCUSSION = 'discussion';
/**
* Get a short label describing this recommendation
*
* eg - Accept Submission
*/
abstract public function getRecommendationLabel(): string;
/**
* Validate the action to create a discussion with this recommendation
*/
public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null)
{
foreach ((array) $props['actions'] as $index => $action) {
switch ($action['id']) {
case $this->ACTION_DISCUSSION:
$errors = $this->validateEmailAction($action, $submission, $this->getAllowedAttachmentFileStages());
if (count($errors)) {
foreach ($errors as $key => $error) {
$validator->errors()->add('actions.' . $index . '.' . $key, $error);
}
}
break;
}
}
}
public function runAdditionalActions(Decision $decision, Submission $submission, User $editor, Context $context, array $actions)
{
parent::runAdditionalActions($decision, $submission, $editor, $context, $actions);
foreach ($actions as $action) {
switch ($action['id']) {
case $this->ACTION_DISCUSSION:
$this->addRecommendationQuery(
$this->getEmailDataFromAction($action),
$submission,
$editor,
$context
);
break;
}
}
}
public function getSteps(Submission $submission, Context $context, User $editor, ?ReviewRound $reviewRound): Steps
{
$steps = new Steps($this, $submission, $context, $reviewRound);
$fakeDecision = $this->getFakeDecision($submission, $editor);
$fileAttachers = $this->getFileAttachers($submission, $context, $reviewRound);
$editors = $steps->getDecidingEditors();
$reviewAssignments = $this->getReviewAssignments($submission->getId(), $reviewRound->getId(), DecisionType::REVIEW_ASSIGNMENT_COMPLETED);
$mailable = new RecommendationNotifyEditors($context, $submission, $fakeDecision, $reviewAssignments);
$steps->addStep((new Email(
$this->ACTION_DISCUSSION,
__('editor.submissionReview.recordRecommendation.notifyEditors'),
__('editor.submission.recommend.notifyEditors.description'),
$editors,
$mailable
->sender($editor)
->recipients($editors),
$context->getSupportedFormLocales(),
$fileAttachers
))->canSkip(false));
return $steps;
}
/**
* Create a query (discussion) among deciding editors
* add attachments to the head note and send email
*/
protected function addRecommendationQuery(EmailData $email, Submission $submission, User $editor, Context $context): void
{
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var \StageAssignmentDAO $stageAssignmentDao */
$queryParticipantIds = [];
$editorsStageAssignments = $stageAssignmentDao->getEditorsAssignedToStage($submission->getId(), $this->getStageId());
foreach ($editorsStageAssignments as $editorsStageAssignment) {
if (!$editorsStageAssignment->getRecommendOnly()) {
if (!in_array($editorsStageAssignment->getUserId(), $queryParticipantIds)) {
$queryParticipantIds[] = $editorsStageAssignment->getUserId();
}
}
}
/** @var QueryDAO $queryDao */
$queryDao = DAORegistry::getDAO('QueryDAO');
$queryId = $queryDao->addQuery(
$submission->getId(),
$this->getStageId(),
$email->subject,
$email->body,
$editor,
$queryParticipantIds,
$context->getId(),
false
);
$query = $queryDao->getById($queryId);
$note = $query->getHeadNote();
$mailable = new Mailable();
foreach ($email->attachments as $attachment) {
if (isset($attachment[Mailable::ATTACHMENT_TEMPORARY_FILE])) {
$temporaryFileManager = new TemporaryFileManager();
$temporaryFile = $temporaryFileManager->getFile($attachment[Mailable::ATTACHMENT_TEMPORARY_FILE], $editor->getId());
if (!$temporaryFile) {
throw new Exception('Could not find temporary file ' . $attachment[Mailable::ATTACHMENT_TEMPORARY_FILE] . ' to attach to the query note.');
}
$this->addSubmissionFileToNoteFromFilePath(
$temporaryFile->getFilePath(),
$attachment['name'],
$note,
$editor,
$submission,
$context
);
$mailable->attachTemporaryFile($attachment[Mailable::ATTACHMENT_TEMPORARY_FILE], $attachment['name'], $editor->getId());
} elseif (isset($attachment[Mailable::ATTACHMENT_SUBMISSION_FILE])) {
$submissionFile = Repo::submissionFile()->get($attachment[Mailable::ATTACHMENT_SUBMISSION_FILE]);
if (!$submissionFile || $submissionFile->getData('submissionId') !== $submission->getId()) {
throw new Exception('Could not find submission file ' . $attachment[Mailable::ATTACHMENT_SUBMISSION_FILE] . ' to attach to the query note.');
}
$newSubmissionFile = clone $submissionFile;
$newSubmissionFile->setData('fileStage', SubmissionFile::SUBMISSION_FILE_QUERY);
$newSubmissionFile->setData('sourceSubmissionFileId', $submissionFile->getId());
$newSubmissionFile->setData('assocType', Application::ASSOC_TYPE_NOTE);
$newSubmissionFile->setData('assocId', $note->getId());
Repo::submissionFile()->add($newSubmissionFile);
$mailable->attachSubmissionFile($newSubmissionFile->getId(), $newSubmissionFile->getLocalizedData('name'));
} elseif (isset($attachment[Mailable::ATTACHMENT_LIBRARY_FILE])) {
/** @var \PKP\context\LibraryFileDAO $libraryFileDao */
$libraryFileDao = DAORegistry::getDAO('LibraryFileDAO');
/** @var \PKP\context\LibraryFile $file */
$libraryFile = $libraryFileDao->getById($attachment[Mailable::ATTACHMENT_LIBRARY_FILE]);
if (!$libraryFile) {
throw new Exception('Could not find library file ' . $attachment[Mailable::ATTACHMENT_LIBRARY_FILE] . ' to attach to the query note.');
}
$this->addSubmissionFileToNoteFromFilePath(
$libraryFile->getFilePath(),
$attachment['name'],
$note,
$editor,
$submission,
$context
);
$mailable->attachLibraryFile($attachment[Mailable::ATTACHMENT_LIBRARY_FILE], $attachment['name']);
}
}
$this->sendEditorsEmail($mailable, $email, $editor, $queryParticipantIds);
}
/**
* Helper function to save a file to the file system and then
* use that in a new submission file attached to the query note
*/
protected function addSubmissionFileToNoteFromFilePath(string $filepath, string $filename, Note $note, User $uploader, Submission $submission, Context $context)
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$submissionDir = Repo::submissionFile()->getSubmissionDir($context->getId(), $submission->getId());
$fileId = Services::get('file')->add(
$filepath,
$submissionDir . '/' . uniqid() . '.' . $extension
);
$submissionFile = Repo::submissionFile()->newDataObject([
'fileId' => $fileId,
'name' => [
Locale::getLocale() => $filename
],
'fileStage' => SubmissionFile::SUBMISSION_FILE_QUERY,
'submissionId' => $submission->getId(),
'uploaderUserId' => $uploader->getId(),
'assocType' => Application::ASSOC_TYPE_NOTE,
'assocId' => $note->getId(),
]);
Repo::submissionFile()->add($submissionFile);
}
/**
* Sends email to editors with the recommendation
*/
protected function sendEditorsEmail(Mailable $mailable, EmailData $email, User $editor, array $recipientIds)
{
$recipients = Repo::user()->getCollector()->filterByUserIds($recipientIds)->getMany();
$mailable
->from($editor->getEmail(), $editor->getFullName())
->to($recipients->map(fn(User $recipient) => ['email' => $recipient->getEmail(), 'name' => $recipient->getFullName()])->toArray())
->cc($email->cc)
->bcc($email->bcc)
->subject($email->subject)
->body($email->body);
Mail::send($mailable);
}
}
@@ -0,0 +1,147 @@
<?php
/**
* @file classes/decision/types/traits/NotifyAuthors.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions for decisions that may request a payment
*/
namespace PKP\decision\types\traits;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Validator;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\log\SubmissionEmailLogDAO;
use PKP\log\SubmissionEmailLogEntry;
use PKP\mail\EmailData;
use PKP\mail\Mailable;
use PKP\mail\mailables\DecisionNotifyOtherAuthors;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
trait NotifyAuthors
{
protected string $ACTION_NOTIFY_AUTHORS = 'notifyAuthors';
/** @copydoc DecisionType::getStageId() */
abstract public function getStageId(): int;
/** @copydoc DecisionType::addEmailDataToMailable() */
abstract protected function addEmailDataToMailable(Mailable $mailable, User $user, EmailData $email): Mailable;
/** @copydoc DecisionType::getAssignedAuthorIds() */
abstract protected function getAssignedAuthorIds(Submission $submission): array;
/**
* Validate the decision action to notify authors
*/
protected function validateNotifyAuthorsAction(array $action, string $actionErrorKey, Validator $validator, Submission $submission)
{
$errors = $this->validateEmailAction($action, $submission, $this->getAllowedAttachmentFileStages());
foreach ($errors as $key => $propErrors) {
foreach ($propErrors as $propError) {
$validator->errors()->add($actionErrorKey . '.' . $key, $propError);
}
}
}
/**
* Send the email to the author(s)
*/
protected function sendAuthorEmail(Mailable $mailable, EmailData $email, User $editor, Submission $submission, Context $context)
{
$recipients = array_map(function ($userId) {
return Repo::user()->get($userId);
}, $this->getAssignedAuthorIds($submission));
$mailable = $this->addEmailDataToMailable($mailable, $editor, $email);
Mail::send($mailable->recipients($recipients, $email->locale));
/** @var SubmissionEmailLogDAO $submissionEmailLogDao */
$submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO');
$submissionEmailLogDao->logMailable(
SubmissionEmailLogEntry::SUBMISSION_EMAIL_EDITOR_NOTIFY_AUTHOR,
$mailable,
$submission,
$editor
);
if ($context->getData('notifyAllAuthors')) {
$authors = $submission->getCurrentPublication()->getData('authors');
$assignedAuthorEmails = array_map(function (User $user) {
return $user->getEmail();
}, $recipients);
$assignedAuthorIds = array_unique($this->getAssignedAuthorIds($submission), SORT_NUMERIC);
$assignedAuthors = Repo::user()->getCollector()->filterByUserIds($assignedAuthorIds)->getMany()->toArray();
$mailable = new DecisionNotifyOtherAuthors($context, $submission, $assignedAuthors);
$emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
$mailable
->sender($editor)
->subject($email->subject)
->body($emailTemplate->getLocalizedData('body'))
->addData([
$mailable::MESSAGE_TO_SUBMITTING_AUTHOR => $email->body,
]);
foreach ($authors as $author) {
if (!$author->getEmail() || in_array($author->getEmail(), $assignedAuthorEmails)) {
continue;
}
$mailable->to($author->getEmail(), $author->getFullName());
Mail::send($mailable);
}
}
}
/**
* Share reviewer file attachments with author
*
* This method looks in the email attachments for any files in the
* SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT stage and sets
* their viewable flag to true. This flag makes the file visible to
* the author from the author submission dashboard.
*/
protected function shareReviewAttachmentFiles(array $attachments, Submission $submission, int $reviewRoundId)
{
if (!in_array($this->getStageId(), [WORKFLOW_STAGE_ID_INTERNAL_REVIEW, WORKFLOW_STAGE_ID_EXTERNAL_REVIEW])) {
return;
}
$submissionFileIds = [];
foreach ($attachments as $attachment) {
if (!isset($attachment['submissionFileId'])) {
continue;
}
$submissionFileIds[] = (int) $attachment['submissionFileId'];
}
if (empty($submissionFileIds)) {
return;
}
$reviewAttachmentIds = Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByReviewRoundIds([$reviewRoundId])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT])
->getIds();
foreach ($reviewAttachmentIds->intersect($submissionFileIds) as $sharedFileId) {
$submissionFile = Repo::submissionFile()->get($sharedFileId);
Repo::submissionFile()->edit(
$submissionFile,
['viewable' => true],
);
}
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @file classes/decision/types/traits/NotifyReviewers.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions for decisions that send a notification to reviewers.
*/
namespace PKP\decision\types\traits;
use APP\core\Application;
use APP\facades\Repo;
use APP\log\event\SubmissionEventLogEntry;
use APP\submission\Submission;
use Illuminate\Support\Facades\Mail;
use Illuminate\Validation\Validator;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
use PKP\log\event\PKPSubmissionEventLogEntry;
use PKP\log\SubmissionLog;
use PKP\mail\EmailData;
use PKP\mail\mailables\DecisionNotifyReviewer;
use PKP\mail\mailables\ReviewerUnassign;
use PKP\security\Validation;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\user\User;
trait NotifyReviewers
{
protected string $ACTION_NOTIFY_REVIEWERS = 'notifyReviewers';
/**
* Send the email to the reviewers
*/
protected function sendReviewersEmail(DecisionNotifyReviewer|ReviewerUnassign $mailable, EmailData $email, User $editor, Submission $submission)
{
/** @var DecisionNotifyReviewer $mailable */
$mailable = $this->addEmailDataToMailable($mailable, $editor, $email);
/** @var User[] $recipients */
$recipients = array_map(function ($userId) {
return Repo::user()->get($userId);
}, $email->recipients);
foreach ($recipients as $recipient) {
Mail::send($mailable->recipients([$recipient], $email->locale));
// Update the ReviewAssignment to indicate the reviewer has been acknowledged
if (is_a($mailable, DecisionNotifyReviewer::class)) {
/** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO');
$reviewAssignment = $reviewAssignmentDao->getReviewAssignment($mailable->getDecision()->getData('reviewRoundId'), $recipient->getId());
if ($reviewAssignment) {
$reviewAssignment->setDateAcknowledged(Core::getCurrentDate());
$reviewAssignment->stampModified();
$reviewAssignmentDao->updateObject($reviewAssignment);
}
}
}
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => PKPSubmissionEventLogEntry::SUBMISSION_LOG_DECISION_EMAIL_SENT,
'userId' => Validation::loggedInAs() ?? Application::get()->getRequest()->getUser()?->getId(),
'message' => 'submission.event.decisionReviewerEmailSent',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
'recipientCount' => count($recipients),
'subject' => $email->subject,
]);
Repo::eventLog()->add($eventLog);
}
/**
* Validate the decision action to notify reviewers
*/
protected function validateNotifyReviewersAction(array $action, string $actionErrorKey, Validator $validator, Submission $submission, int $reviewRoundId, string $reviewAssignmentStatus)
{
$errors = $this->validateEmailAction($action, $submission, $this->getAllowedAttachmentFileStages());
foreach ($errors as $key => $propErrors) {
foreach ($propErrors as $propError) {
$validator->errors()->add($actionErrorKey . '.' . $key, $propError);
}
}
if (empty($action['recipients'])) {
$validator->errors()->add($actionErrorKey . '.recipients', __('validator.required'));
return;
}
$reviewerIds = $this->getReviewerIds($submission->getId(), $reviewRoundId, $reviewAssignmentStatus);
$invalidRecipients = array_diff($action['recipients'], $reviewerIds);
if (count($invalidRecipients)) {
$this->setRecipientError($actionErrorKey, $invalidRecipients, $validator);
}
}
}
@@ -0,0 +1,80 @@
<?php
/**
* @file classes/decision/types/traits/WithReviewAssignments.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class decision
*
* @brief Helper functions to provide review assignments related details associated with submission
*/
namespace PKP\decision\types\traits;
use Exception;
use PKP\db\DAORegistry;
use PKP\decision\DecisionType;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
trait WithReviewAssignments
{
/**
* Get all the review assignments based on review assignment states
*
* @param int $submissionId The targeted submission id
* @param int $reviewRoundId The targeted review round id
* @param int $reviewAssignmentStatus One of the DecisionType::REVIEW_ASSIGNMENT_STATUS_* constants
*
* @throws \Exception
*
* @return ReviewAssignment[]
*
*/
protected function getReviewAssignments(int $submissionId, int $reviewRoundId, int $reviewAssignmentStatus): array
{
/** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO');
$reviewAssignments = $reviewAssignmentDao->getBySubmissionId($submissionId, $reviewRoundId, $this->getStageId());
$assignments = [];
foreach ($reviewAssignments as $reviewAssignment) {
$valid = match ($reviewAssignmentStatus) {
DecisionType::REVIEW_ASSIGNMENT_COMPLETED => in_array($reviewAssignment->getStatus(), ReviewAssignment::REVIEW_COMPLETE_STATUSES),
DecisionType::REVIEW_ASSIGNMENT_ACTIVE => !$reviewAssignment->getDeclined() && !$reviewAssignment->getCancelled(),
DecisionType::REVIEW_ASSIGNMENT_CONFIRMED => $reviewAssignment->getDateConfirmed() && !$reviewAssignment->getCancelled(),
default => throw new Exception('Invalid review assignment state'),
};
if (!$valid) {
continue;
}
$assignments[] = $reviewAssignment;
}
return $assignments;
}
/**
* Get all the reviewers id based on review assignment states
*
* @param int $submissionId The targeted submission id
* @param int $reviewRoundId The targeted review round id
* @param int $reviewAssignmentStatus One of the DecisionType::REVIEW_ASSIGNMENT_STATUS_* constants
*
* @return array<int>
*/
protected function getReviewerIds(int $submissionId, int $reviewRoundId, int $reviewAssignmentStatus): array
{
$assignments = $this->getReviewAssignments($submissionId, $reviewRoundId, $reviewAssignmentStatus);
return collect($assignments)
->map(fn ($assignment) => $assignment->getReviewerId())
->toArray();
}
}