first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user