first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+539
View File
@@ -0,0 +1,539 @@
<?php
/**
* @file classes/submission/Collector.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 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 submissions
*/
namespace PKP\submission;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Collector as AppCollector;
use APP\submission\Submission;
use Exception;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\Core;
use PKP\core\interfaces\CollectorInterface;
use PKP\facades\Locale;
use PKP\identity\Identity;
use PKP\plugins\Hook;
use PKP\search\SubmissionSearch;
use PKP\security\Role;
use PKP\submission\reviewRound\ReviewRound;
/**
* @template T of Submission
*/
abstract class Collector implements CollectorInterface
{
public const ORDERBY_DATE_PUBLISHED = 'datePublished';
public const ORDERBY_DATE_SUBMITTED = 'dateSubmitted';
public const ORDERBY_LAST_ACTIVITY = 'lastActivity';
public const ORDERBY_LAST_MODIFIED = 'lastModified';
public const ORDERBY_SEQUENCE = 'sequence';
public const ORDERBY_TITLE = 'title';
public const ORDERBY_SEARCH_RANKING = 'ranking';
public const ORDER_DIR_ASC = 'ASC';
public const ORDER_DIR_DESC = 'DESC';
public const UNASSIGNED = -1;
public DAO $dao;
public ?array $categoryIds = null;
public ?array $contextIds = null;
public ?int $count = null;
public ?int $daysInactive = null;
public bool $isIncomplete = false;
public bool $isOverdue = false;
public ?int $offset = null;
public string $orderBy = self::ORDERBY_DATE_SUBMITTED;
public string $orderDirection = 'DESC';
public ?string $searchPhrase = null;
public ?int $maxSearchKeywords = null;
public ?array $statuses = null;
public ?array $stageIds = null;
public ?array $doiStatuses = null;
public ?bool $hasDois = null;
public ?array $excludeIds = null;
/** @var array Which DOI types should be considered when checking if a submission has DOIs set */
public array $enabledDoiTypes = [];
/** @var array|int */
public $assignedTo = 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);
}
/**
* Limit results to submissions in these contexts
*/
public function filterByContextIds(?array $contextIds): AppCollector
{
$this->contextIds = $contextIds;
return $this;
}
/**
* Limit results by submissions assigned to these categories
*/
public function filterByCategoryIds(?array $categoryIds): AppCollector
{
$this->categoryIds = $categoryIds;
return $this;
}
/**
* Limit results to submissions that contain any pub objects (e.g. publication and galley) with these statuses
*
* @param array|null $statuses One or more of DOI::STATUS_* constants
*
*/
public function filterByDoiStatuses(?array $statuses): AppCollector
{
$this->doiStatuses = $statuses;
return $this;
}
/**
* Limit results to submissions that do/don't have any DOIs assign to their sub objects
*
* @param array|null $enabledDoiTypes TYPE_* constants to consider when checking submission has DOIs
*/
public function filterByHasDois(?bool $hasDois, ?array $enabledDoiTypes = null): AppCollector
{
$this->hasDois = $hasDois;
$this->enabledDoiTypes = $enabledDoiTypes === null ? [Repo::doi()::TYPE_PUBLICATION] : $enabledDoiTypes;
return $this;
}
/**
* Limit results by submissions with these statuses
*
* @see \PKP\submissions\PKPSubmission::STATUS_
*/
public function filterByStatus(?array $statuses): AppCollector
{
$this->statuses = $statuses;
return $this;
}
/**
* Limit results by submissions in these workflow stage ids
*/
public function filterByStageIds(?array $stageIds): AppCollector
{
$this->stageIds = $stageIds;
return $this;
}
/**
* Limit results to incomplete submissions
*
* Submissions are incomplete when the author has begun to enter
* details about their submission but not yet submitted it.
*/
public function filterByIncomplete(bool $isIncomplete): AppCollector
{
$this->isIncomplete = $isIncomplete;
return $this;
}
/**
* Limit results to submissions with overdue tasks
*/
public function filterByOverdue(bool $isOverdue): AppCollector
{
$this->isOverdue = $isOverdue;
return $this;
}
/**
* Limit results to submission with no activity for X days
*/
public function filterByDaysInactive(?int $daysInactive): AppCollector
{
$this->daysInactive = $daysInactive;
return $this;
}
/**
* Limit results to submissions assigned to these users
*
* @param int|array $assignedTo An array of user IDs
* or self::UNASSIGNED to get unassigned submissions
*/
public function assignedTo($assignedTo): AppCollector
{
$this->assignedTo = $assignedTo;
return $this;
}
/**
* Limit results to submissions matching this search query
*/
public function searchPhrase(?string $phrase, ?int $maxSearchKeywords = null): AppCollector
{
$this->searchPhrase = $phrase;
$this->maxSearchKeywords = $maxSearchKeywords;
return $this;
}
/**
* Ensure the given submission IDs are not included
*/
public function excludeIds(?array $ids): AppCollector
{
$this->excludeIds = $ids;
return $this;
}
/**
* Limit the number of objects retrieved
*/
public function limit(?int $count): AppCollector
{
$this->count = $count;
return $this;
}
/**
* Offset the number of objects retrieved, for example to
* retrieve the second page of contents
*/
public function offset(?int $offset): AppCollector
{
$this->offset = $offset;
return $this;
}
/**
* Order the results
*
* The following column values are supported:
*
* - lastModified
* - dateLastActivity
* - title
* - seq (sequence)
* - DAO::ORDERBY_DATE_PUBLISHED
*
* Results are ordered by the date submitted by default.
*
* @param string $sorter One of the self::ORDERBY_ constants
* @param string $direction One of the self::ORDER_DIR_ constants
*/
public function orderBy(string $sorter, string $direction = self::ORDER_DIR_DESC): AppCollector
{
$this->orderBy = $sorter;
$this->orderDirection = $direction;
return $this;
}
/**
* Add APP-specific filtering methods for submission sub objects DOI statuses
*
*/
abstract protected function addDoiStatusFilterToQuery(Builder $q);
/**
* Add APP-specific filtering methods for checking if submission sub objects have DOIs assigned
*/
abstract protected function addHasDoisFilterToQuery(Builder $q);
/**
* @copydoc CollectorInterface::getQueryBuilder()
*/
public function getQueryBuilder(): Builder
{
$q = DB::table('submissions AS s')
->leftJoin('publications AS po', 's.current_publication_id', '=', 'po.publication_id')
->select(['s.*']);
// Never permit a query without a context_id unless the CONTEXT_ID_ALL wildcard
// has been set explicitly.
if (!isset($this->contextIds)) {
throw new Exception('Submissions can not be retrieved without a context id. Pass the CONTEXT_ID_ALL wildcard to get submissions from any context.');
} elseif (!in_array(Application::CONTEXT_ID_ALL, $this->contextIds)) {
$q->whereIn('s.context_id', $this->contextIds);
}
// Prepare keywords (allows short and numeric words)
$keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true))
->unique()
->take($this->maxSearchKeywords ?? PHP_INT_MAX);
// Setup the order by
switch ($this->orderBy) {
case self::ORDERBY_DATE_PUBLISHED:
$q->addSelect(['po.date_published']);
$q->orderBy('po.date_published', $this->orderDirection);
break;
case self::ORDERBY_LAST_ACTIVITY:
$q->orderBy('s.date_last_activity', $this->orderDirection);
break;
case self::ORDERBY_LAST_MODIFIED:
$q->orderBy('s.last_modified', $this->orderDirection);
break;
case self::ORDERBY_SEQUENCE:
$q->addSelect(['po.seq']);
$q->orderBy('po.seq', $this->orderDirection);
break;
case self::ORDERBY_TITLE:
$locale = Locale::getLocale();
$q->leftJoin('publications as publication_tlp', 's.current_publication_id', '=', 'publication_tlp.publication_id')
->leftJoin('publication_settings as publication_tlps', fn (JoinClause $join) =>
$join->on('publication_tlp.publication_id', '=', 'publication_tlps.publication_id')
->where('publication_tlps.setting_name', '=', 'title')
->where('publication_tlps.setting_value', '!=', '')
->where('publication_tlps.locale', '=', $locale)
);
$q->leftJoin('publications as publication_tlpl', 's.current_publication_id', '=', 'publication_tlpl.publication_id')
->leftJoin('publication_settings as publication_tlpsl', fn (JoinClause $join) =>
$join->on('publication_tlp.publication_id', '=', 'publication_tlpsl.publication_id')
->on('publication_tlpsl.locale', '=', 's.locale')
->where('publication_tlpsl.setting_name', '=', 'title')
);
$coalesceTitles = 'COALESCE(publication_tlps.setting_value, publication_tlpsl.setting_value)';
$q->addSelect([DB::raw($coalesceTitles)]);
$q->orderBy(DB::raw($coalesceTitles), $this->orderDirection);
break;
case self::ORDERBY_SEARCH_RANKING:
if (!$keywords->count()) {
$q->orderBy('s.date_submitted', $this->orderDirection);
break;
}
// Retrieves the number of matches for all keywords
$orderByMatchCount = DB::table('submission_search_objects', 'sso')
->join('submission_search_object_keywords AS ssok', 'ssok.object_id', '=', 'sso.object_id')
->join('submission_search_keyword_list AS sskl', 'sskl.keyword_id', '=', 'ssok.keyword_id')
->where(fn (Builder $q) =>
$keywords->map(fn (string $keyword) => $q
->orWhere('sskl.keyword_text', '=', DB::raw('LOWER(?)'))
->addBinding($keyword)
)
)
->whereColumn('s.submission_id', '=', 'sso.submission_id')
->selectRaw('COUNT(0)');
// Retrieves the number of distinct matched keywords
$orderByDistinctKeyword = (clone $orderByMatchCount)->select(DB::raw('COUNT(DISTINCT sskl.keyword_id)'));
$q->orderBy($orderByDistinctKeyword, $this->orderDirection)
->orderBy($orderByMatchCount, $this->orderDirection);
break;
case self::ORDERBY_DATE_SUBMITTED:
default:
$q->orderBy('s.date_submitted', $this->orderDirection);
break;
}
if (isset($this->statuses)) {
$q->whereIn('s.status', $this->statuses);
}
if (isset($this->stageIds)) {
$q->whereIn('s.stage_id', $this->stageIds);
}
if ($this->isIncomplete) {
$q->where('s.submission_progress', '<>', '');
}
if (isset($this->daysInactive)) {
$q->where('s.date_last_activity', '<', Core::getCurrentDate(strtotime('-' . $this->daysInactive . ' days')));
}
if ($this->isOverdue) {
$q->leftJoin('review_assignments as raod', 'raod.submission_id', '=', 's.submission_id')
->leftJoin('review_rounds as rr', fn (Builder $table) =>
$table->on('rr.submission_id', '=', 's.submission_id')
->on('raod.review_round_id', '=', 'rr.review_round_id')
);
// Only get overdue assignments on active review rounds
$q->whereNotIn('rr.status', [
ReviewRound::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW,
ReviewRound::REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL,
ReviewRound::REVIEW_ROUND_STATUS_ACCEPTED,
ReviewRound::REVIEW_ROUND_STATUS_DECLINED,
]);
$q->where(fn (Builder $q) =>
$q->where('raod.declined', '<>', 1)
->where('raod.cancelled', '<>', 1)
->where(fn (Builder $q) =>
$q->where('raod.date_due', '<', Core::getCurrentDate(strtotime('tomorrow')))
->whereNull('raod.date_completed')
)
->orWhere(fn (Builder $q) =>
$q->where('raod.date_response_due', '<', Core::getCurrentDate(strtotime('tomorrow')))
->whereNull('raod.date_confirmed')
)
);
}
if (is_array($this->assignedTo)) {
$q->whereIn('s.submission_id', fn (Builder $q) =>
$q->select('s.submission_id')
->from('submissions AS s')
->leftJoin('stage_assignments as sa', fn (Builder $q) =>
$q->on('s.submission_id', '=', 'sa.submission_id')
->whereIn('sa.user_id', $this->assignedTo)
)
->leftJoin('review_assignments as ra', fn (Builder $table) =>
$table->on('s.submission_id', '=', 'ra.submission_id')
->where('ra.declined', '=', (int) 0)
->where('ra.cancelled', '=', (int) 0)
->whereIn('ra.reviewer_id', $this->assignedTo)
)
->whereNotNull('sa.stage_assignment_id')
->orWhereNotNull('ra.review_id')
);
} elseif ($this->assignedTo === self::UNASSIGNED) {
$sub = DB::table('stage_assignments')
->select(DB::raw('count(stage_assignments.stage_assignment_id)'))
->leftJoin('user_groups', 'stage_assignments.user_group_id', '=', 'user_groups.user_group_id')
->where('stage_assignments.submission_id', '=', DB::raw('s.submission_id'))
->whereIn('user_groups.role_id', [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]);
$q->whereNotNull('s.date_submitted')
->mergeBindings($sub)
->where(DB::raw('(' . $sub->toSql() . ')'), '=', '0');
}
// Search phrase
if ($keywords->count()) {
$likePattern = DB::raw("CONCAT('%', LOWER(?), '%')");
if(!empty($this->assignedTo)) {
// Holds a single random row to check whether we have any assignment
$q->leftJoinSub(fn (Builder $q) => $q
->from('review_assignments', 'ra')
->whereIn('ra.reviewer_id', $this->assignedTo == self::UNASSIGNED ? [] : (array) $this->assignedTo)
->select(DB::raw('1 AS value'))
->limit(1),
'any_assignment', 'any_assignment.value', '=', DB::raw('1')
);
}
// Builds the filters
$q->where(fn (Builder $q) => $keywords
->map(fn (string $keyword) => $q
// Look for matches on the indexed data
->orWhereExists(fn (Builder $query) => $query
->from('submission_search_objects', 'sso')
->join('submission_search_object_keywords AS ssok', 'sso.object_id', '=', 'ssok.object_id')
->join('submission_search_keyword_list AS sskl', 'sskl.keyword_id', '=', 'ssok.keyword_id')
->where('sskl.keyword_text', '=', DB::raw('LOWER(?)'))->addBinding($keyword)
->whereColumn('s.submission_id', '=', 'sso.submission_id')
// Don't permit reviewers to search on author names
->when(!empty($this->assignedTo), fn (Builder $q) => $q
->where(fn (Builder $q) => $q
->whereNull('any_assignment.value')
->orWhere('sso.type', '!=', SubmissionSearch::SUBMISSION_SEARCH_AUTHOR)
)
)
)
// Search on the publication title
->orWhereIn('s.submission_id', fn (Builder $query) => $query
->select('p.submission_id')->from('publications AS p')
->join('publication_settings AS ps', 'p.publication_id', '=', 'ps.publication_id')
->where('ps.setting_name', '=', 'title')
->where(DB::raw('LOWER(ps.setting_value)'), 'LIKE', $likePattern)
->addBinding($keyword)
)
// Search on the author name and ORCID
->orWhereIn('s.submission_id', fn (Builder $query) => $query
->select('p.submission_id')
->from('publications AS p')
->join('authors AS au', 'au.publication_id', '=', 'p.publication_id')
->join('author_settings AS aus', 'aus.author_id', '=', 'au.author_id')
->whereIn('aus.setting_name', [
Identity::IDENTITY_SETTING_GIVENNAME,
Identity::IDENTITY_SETTING_FAMILYNAME,
'orcid'
])
// Don't permit reviewers to search on author names
->when(!empty($this->assignedTo), fn (Builder $q) => $q
->where(fn (Builder $q) => $q
->whereNull('any_assignment.value')
->orWhereNotIn('aus.setting_name', [
Identity::IDENTITY_SETTING_GIVENNAME,
Identity::IDENTITY_SETTING_FAMILYNAME
])
)
)
->where(DB::raw('LOWER(aus.setting_value)'), 'LIKE', $likePattern)
->addBinding($keyword)
)
// Search for the exact submission ID
->when(
($numericWords = $keywords->filter(fn (string $keyword) => ctype_digit($keyword)))->count(),
fn (Builder $query) => $query->orWhereIn('s.submission_id', $numericWords)
)
)
);
} elseif (strlen($this->searchPhrase ?? '')) {
// If there's search text, but no keywords could be extracted from it, force the query to return nothing
$q->whereRaw('1 = 0');
}
if (isset($this->categoryIds)) {
$q->join('publication_categories as pc', 's.current_publication_id', '=', 'pc.publication_id')
->whereIn('pc.category_id', $this->categoryIds);
}
// Filter by any child pub object's DOI status
$q->when($this->doiStatuses !== null, fn (Builder $q) => $this->addDoiStatusFilterToQuery($q));
// Filter by whether any child pub objects have DOIs assigned
$q->when($this->hasDois !== null, fn (Builder $q) => $this->addHasDoisFilterToQuery($q));
// Filter out excluded submission IDs
$q->when($this->excludeIds !== null, fn (Builder $q) => $q->whereNotIn('s.submission_id', $this->excludeIds));
// Limit and offset results for pagination
if (isset($this->count)) {
$q->limit($this->count);
}
if (isset($this->offset)) {
$q->offset($this->offset);
}
// Add app-specific query statements
Hook::call('Submission::Collector', [&$q, $this]);
return $q;
}
}
+312
View File
@@ -0,0 +1,312 @@
<?php
/**
* @file classes/submission/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @brief Read and write submissions to the database.
*/
namespace PKP\submission;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Collector;
use APP\submission\Submission;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\EntityDAO;
use PKP\core\traits\EntityWithParent;
use PKP\db\DAORegistry;
use PKP\log\event\EventLogEntry;
use PKP\log\SubmissionEmailLogDAO;
use PKP\note\NoteDAO;
use PKP\notification\NotificationDAO;
use PKP\query\QueryDAO;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
/**
* @template T of Submission
* @extends EntityDAO<T>
*/
class DAO extends EntityDAO
{
use EntityWithParent;
/** @copydoc EntityDAO::$schema */
public $schema = PKPSchemaService::SCHEMA_SUBMISSION;
/** @copydoc EntityDAO::$table */
public $table = 'submissions';
/** @copydoc EntityDAO::$settingsTable */
public $settingsTable = 'submission_settings';
/** @copydoc EntityDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'submission_id';
/** @copydoc SchemaDAO::$primaryTableColumns */
public $primaryTableColumns = [
'id' => 'submission_id',
'contextId' => 'context_id',
'currentPublicationId' => 'current_publication_id',
'dateLastActivity' => 'date_last_activity',
'dateSubmitted' => 'date_submitted',
'lastModified' => 'last_modified',
'locale' => 'locale',
'stageId' => 'stage_id',
'status' => 'status',
'submissionProgress' => 'submission_progress',
];
/**
* Get the parent object ID column name
*/
public function getParentColumn(): string
{
return 'context_id';
}
/**
* Instantiate a new DataObject
*/
public function newDataObject(): Submission
{
return app(Submission::class);
}
/**
* Get the total count of submissions 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('s.' . $this->primaryKeyColumn)
->pluck('s.' . $this->primaryKeyColumn);
}
/**
* Get a collection of announcements 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->submission_id => $this->fromRow($row);
}
});
}
/**
* Get the submission id by its url path
*/
public function getIdByUrlPath(string $urlPath, int $contextId): ?int
{
$publication = DB::table('publications as p')
->leftJoin('submissions as s', 's.submission_id', '=', 'p.submission_id')
->where('s.context_id', '=', $contextId)
->where('p.url_path', '=', $urlPath)
->first();
return $publication
? $publication->submission_id
: null;
}
/**
* Get submission ids that have a matching setting
*/
public function getIdsBySetting(string $settingName, $settingValue, int $contextId): Enumerable
{
return DB::table($this->table . ' as s')
->join($this->settingsTable . ' as ss', 's.submission_id', '=', 'ss.submission_id')
->where('ss.setting_name', '=', $settingName)
->where('ss.setting_value', '=', $settingValue)
->where('s.context_id', '=', (int) $contextId)
->select('s.submission_id')
->pluck('s.submission_id');
}
/**
* Retrieve a submission by public id
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param null|mixed $contextId
*/
public function getByPubId(string $pubIdType, string $pubId, $contextId = null): ?Submission
{
// Add check for incoming DOI request for legacy calls that bypass the Submission Repository
if ($pubIdType == 'doi') {
return $this->getByDoi($pubId, $contextId);
} else {
$qb = DB::table('publication_settings ps')
->join('publications p', 'p.publication_id', '=', 'ps.publication_id')
->join('submissions s', 'p.publication_id', '=', 's.current_publication_id')
->where('ps.setting_name', '=', 'pub-id::' . $pubIdType)
->where('ps.setting_value', '=', $pubId);
if ($contextId) {
$qb->where('s.context_id', '=', (int) $contextId);
}
$row = $qb->get(['s.submission_id'])->first();
return $row
? $this->get($row->submission_id)
: null;
}
}
/**
* Retrieve a submission by its current publication's DOI
*/
public function getByDoi(string $doi, int $contextId): ?Submission
{
$q = DB::table($this->table, 's')
->leftJoin('publications AS p', 'p.publication_id', '=', 's.current_publication_id')
->leftJoin('dois AS d', 'd.doi_id', '=', 'p.doi_id')
->where('d.doi', '=', $doi)
->where('s.context_id', '=', $contextId);
$row = $q->select(['s.submission_id AS submission_id'])->get()->first();
return $row ? $this->get($row->submission_id) : null;
}
/**
* @copydoc EntityDAO::fromRow()
*/
public function fromRow(object $row): Submission
{
$submission = parent::fromRow($row);
$submission->setData(
'publications',
Repo::publication()->getCollector()
->filterBySubmissionIds([$submission->getId()])
->getMany()
->remember()
);
return $submission;
}
/**
* @copydoc EntityDAO::_insert()
*/
public function insert(Submission $submission): int
{
return parent::_insert($submission);
}
/**
* @copydoc EntityDAO::_update()
*/
public function update(Submission $submission)
{
parent::_update($submission);
}
/**
* @copydoc EntityDAO::_delete()
*/
public function delete(Submission $submission)
{
parent::_delete($submission);
}
/**
* @copydoc \PKP\core\EntityDAO::deleteById()
*/
public function deleteById(int $id)
{
$submission = Repo::submission()->get($id);
// Delete publications
$publications = Repo::publication()->getCollector()
->filterBySubmissionIds([$id])
->getMany();
foreach ($publications as $publication) {
Repo::publication()->delete($publication);
}
// Delete submission files.
$submissionFiles = Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->getMany();
foreach ($submissionFiles as $submissionFile) {
Repo::submissionFile()->delete($submissionFile);
}
Repo::decision()->deleteBySubmissionId($id);
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignmentDao->deleteBySubmissionId($id);
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao->deleteBySubmissionId($id);
// Delete the queries associated with a submission
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$queryDao->deleteByAssoc(Application::ASSOC_TYPE_SUBMISSION, $id);
// Delete the stage assignments.
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignments = $stageAssignmentDao->getBySubmissionAndStageId($id);
while ($stageAssignment = $stageAssignments->next()) {
$stageAssignmentDao->deleteObject($stageAssignment);
}
$noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
$noteDao->deleteByAssoc(Application::ASSOC_TYPE_SUBMISSION, $id);
$submissionCommentDao = DAORegistry::getDAO('SubmissionCommentDAO'); /** @var SubmissionCommentDAO $submissionCommentDao */
$submissionCommentDao->deleteBySubmissionId($id);
// Delete any outstanding notifications for this submission
$notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
$notificationDao->deleteByAssoc(Application::ASSOC_TYPE_SUBMISSION, $id);
Repo::eventLog()->getCollector()
->filterByAssoc(Application::ASSOC_TYPE_SUBMISSION, [$id])
->getMany()
->each(function (EventLogEntry $logEntry) {
Repo::eventLog()->delete($logEntry);
});
$submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO'); /** @var SubmissionEmailLogDAO $submissionEmailLogDao */
$submissionEmailLogDao->deleteByAssoc(Application::ASSOC_TYPE_SUBMISSION, $id);
parent::deleteById($id);
}
}
+236
View File
@@ -0,0 +1,236 @@
<?php
/**
* @file classes/submission/Genre.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Genre
*
* @ingroup submission
*
* @see GenreDAO
*
* @brief Basic class describing a genre.
*/
namespace PKP\submission;
use PKP\db\DAORegistry;
class Genre extends \PKP\core\DataObject
{
public const GENRE_CATEGORY_DOCUMENT = 1;
public const GENRE_CATEGORY_ARTWORK = 2;
public const GENRE_CATEGORY_SUPPLEMENTARY = 3;
/**
* Get ID of context.
*
* @return int
*/
public function getContextId()
{
return $this->getData('contextId');
}
/**
* Set ID of context.
*
* @param int $contextId
*/
public function setContextId($contextId)
{
$this->setData('contextId', $contextId);
}
/**
* Get sequence of genre.
*
* @return float
*/
public function getSequence()
{
return $this->getData('sequence');
}
/**
* Set sequence of genre.
*
* @param float $sequence
*/
public function setSequence($sequence)
{
$this->setData('sequence', $sequence);
}
/**
* Get key of genre.
*
* @return string
*/
public function getKey()
{
return $this->getData('key');
}
/**
* Set key of genre.
*
* @param string $key
*/
public function setKey($key)
{
$this->setData('key', $key);
}
/**
* Get enabled status of genre.
*
* @return bool
*/
public function getEnabled()
{
return $this->getData('enabled');
}
/**
* Set enabled status of genre.
*
* @param bool $enabled
*/
public function setEnabled($enabled)
{
$this->setData('enabled', $enabled);
}
/**
* Set the name of the genre
*
* @param string $name
* @param string $locale
*/
public function setName($name, $locale)
{
$this->setData('name', $name, $locale);
}
/**
* Get the name of the genre
*
* @param string $locale
*
* @return string
*/
public function getName($locale)
{
return $this->getData('name', $locale);
}
/**
* Get the localized name of the genre
*
* @return string
*/
public function getLocalizedName()
{
return $this->getLocalizedData('name');
}
/**
* Get context file category (e.g. artwork or document)
*
* @return int GENRE_CATEGORY_...
*/
public function getCategory()
{
return $this->getData('category');
}
/**
* Set context file category (e.g. artwork or document)
*
* @param int $category GENRE_CATEGORY_...
*/
public function setCategory($category)
{
$this->setData('category', $category);
}
/**
* Get dependent flag
*
* @return bool
*/
public function getDependent()
{
return $this->getData('dependent');
}
/**
* Set dependent flag
*
* @param bool $dependent
*/
public function setDependent($dependent)
{
$this->setData('dependent', $dependent);
}
/**
* Get supplementary flag
*
* @return bool
*/
public function getSupplementary()
{
return $this->getData('supplementary');
}
/**
* Set supplementary flag
*
* @param bool $supplementary
*/
public function setSupplementary($supplementary)
{
$this->setData('supplementary', $supplementary);
}
/**
* Get whether this file is required for new submissions
*/
public function getRequired(): bool
{
return (bool) $this->getData('required');
}
/**
* Set whether this file is required for new submissions
*/
public function setRequired(bool $required): void
{
$this->setData('required', $required);
}
/**
* Is this a default genre.
*
* @return bool
*/
public function isDefault()
{
$genreDao = DAORegistry::getDAO('GenreDAO'); /** @var GenreDAO $genreDao */
$defaultKeys = $genreDao->getDefaultKeys();
return in_array($this->getKey(), $defaultKeys);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\Genre', '\Genre');
foreach (['GENRE_CATEGORY_DOCUMENT', 'GENRE_CATEGORY_ARTWORK', 'GENRE_CATEGORY_SUPPLEMENTARY'] as $constantName) {
define($constantName, constant('\Genre::' . $constantName));
}
}
+461
View File
@@ -0,0 +1,461 @@
<?php
/**
* @file classes/submission/GenreDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class GenreDAO
*
* @ingroup submission
*
* @see Genre
*
* @brief Operations for retrieving and modifying Genre objects.
*/
namespace PKP\submission;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use PKP\db\DAO;
use PKP\db\DAOResultFactory;
use PKP\db\XMLDAO;
use PKP\plugins\Hook;
class GenreDAO extends DAO
{
/**
* Retrieve a genre by type id.
*
* @param int $genreId
* @param null|mixed $contextId
*
* @return Genre
*/
public function getById($genreId, $contextId = null)
{
$params = [(int) $genreId];
if ($contextId) {
$params[] = (int) $contextId;
}
$result = $this->retrieve(
'SELECT * FROM genres WHERE genre_id = ?' .
($contextId ? ' AND context_id = ?' : '') .
' ORDER BY seq',
$params
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Retrieve all genres
*
* @param int $contextId
* @param ?\PKP\db\DBResultRange $rangeInfo optional
*
* @return DAOResultFactory<Genre> containing matching genres
*/
public function getEnabledByContextId($contextId, $rangeInfo = null)
{
$result = $this->retrieveRange(
'SELECT * FROM genres
WHERE enabled = ? AND context_id = ?
ORDER BY seq',
[1, (int) $contextId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow', ['id']);
}
/**
* Retrieve genres based on whether they are dependent or not.
*
* @param bool $dependentFilesOnly
* @param int $contextId
* @param ?\PKP\db\DBResultRange $rangeInfo optional
*
* @return DAOResultFactory<Genre> containing matching genres
*/
public function getByDependenceAndContextId($dependentFilesOnly, $contextId, $rangeInfo = null)
{
$result = $this->retrieveRange(
'SELECT * FROM genres
WHERE enabled = ? AND context_id = ? AND dependent = ?
ORDER BY seq',
[1, (int) $contextId, (int) $dependentFilesOnly],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow', ['id']);
}
/**
* Retrieve genres based on whether they are supplementary or not.
*
* @param bool $supplementaryFilesOnly
* @param int $contextId
* @param ?\PKP\db\DBResultRange $rangeInfo optional
*
* @return DAOResultFactory<Genre>
*/
public function getBySupplementaryAndContextId($supplementaryFilesOnly, $contextId, $rangeInfo = null)
{
$result = $this->retrieveRange(
'SELECT * FROM genres
WHERE enabled = ? AND context_id = ? AND supplementary = ?
ORDER BY seq',
[1, (int) $contextId, (int) $supplementaryFilesOnly],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow', ['id']);
}
/**
* Retrieve genres that are not supplementary or dependent.
*
* @param int $contextId
* @param ?\PKP\db\DBResultRange $rangeInfo optional
*
* @return DAOResultFactory<Genre>
*/
public function getPrimaryByContextId($contextId, $rangeInfo = null)
{
$result = $this->retrieveRange(
'SELECT * FROM genres
WHERE enabled = ? AND context_id = ? AND dependent = ? AND supplementary = ?
ORDER BY seq',
[1, (int) $contextId, 0, 0],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow', ['id']);
}
/**
* Retrieve all genres
*
* @param int $contextId
* @param ?\PKP\db\DBResultRange $rangeInfo optional
*
* @return DAOResultFactory<Genre> containing matching genres
*/
public function getByContextId($contextId, $rangeInfo = null)
{
$result = $this->retrieveRange(
'SELECT * FROM genres WHERE context_id = ? ORDER BY seq',
[(int) $contextId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow', ['id']);
}
/**
* Get genres that are required for a new
* submission in a context
*/
public function getRequiredToSubmit(int $contextId): Collection
{
return DB::table('genres')
->where('context_id', $contextId)
->where('required', 1)
->get()
->map(function (object $row) {
return $this->_fromRow((array) $row);
});
}
/**
* Retrieves the genre associated with a key.
*
* @param string $key the entry key
* @param int $contextId Optional context ID
*
* @return Genre
*/
public function getByKey($key, $contextId = null)
{
$params = [$key];
if ($contextId) {
$params[] = (int) $contextId;
}
$result = $this->retrieve(
'SELECT * FROM genres WHERE entry_key = ? ' .
($contextId ? ' AND context_id = ?' : ''),
$params
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Get a list of field names for which data is localized.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['name'];
}
/**
* Update the settings for this object
*
* @param object $genre
*/
public function updateLocaleFields($genre)
{
$this->updateDataObjectSettings(
'genre_settings',
$genre,
['genre_id' => $genre->getId()]
);
}
/**
* Construct a new data object corresponding to this DAO.
*
* @return Genre
*/
public function newDataObject()
{
return new Genre();
}
/**
* Internal function to return a Genre object from a row.
*
* @param array $row
*
* @return Genre
*/
public function _fromRow($row)
{
$genre = $this->newDataObject();
$genre->setId((int) $row['genre_id']);
$genre->setKey($row['entry_key']);
$genre->setContextId($row['context_id']);
$genre->setCategory((int) $row['category']);
$genre->setDependent($row['dependent']);
$genre->setSupplementary($row['supplementary']);
$genre->setRequired($row['required']);
$genre->setSequence($row['seq']);
$genre->setEnabled($row['enabled']);
$this->getDataObjectSettings('genre_settings', 'genre_id', $row['genre_id'], $genre);
Hook::call('GenreDAO::_fromRow', [&$genre, &$row]);
return $genre;
}
/**
* Insert a new genre.
*
* @param Genre $genre
*
* @return int Inserted genre ID
*/
public function insertObject($genre)
{
$this->update(
'INSERT INTO genres
(entry_key, seq, context_id, category, dependent, supplementary, required)
VALUES
(?, ?, ?, ?, ?, ?, ?)',
[
$genre->getKey(),
(float) $genre->getSequence(),
(int) $genre->getContextId(),
(int) $genre->getCategory(),
$genre->getDependent() ? 1 : 0,
$genre->getSupplementary() ? 1 : 0,
$genre->getRequired() ? 1 : 0,
]
);
$genre->setId($this->getInsertId());
$this->updateLocaleFields($genre);
return $genre->getId();
}
/**
* Update an existing genre.
*
* @param Genre $genre
*/
public function updateObject($genre)
{
$this->update(
'UPDATE genres
SET entry_key = ?,
seq = ?,
dependent = ?,
supplementary = ?,
enabled = ?,
category = ?,
required = ?
WHERE genre_id = ?',
[
$genre->getKey(),
(float) $genre->getSequence(),
$genre->getDependent() ? 1 : 0,
$genre->getSupplementary() ? 1 : 0,
$genre->getEnabled() ? 1 : 0,
$genre->getCategory(),
$genre->getRequired() ? 1 : 0,
(int) $genre->getId(),
]
);
$this->updateLocaleFields($genre);
}
/**
* Delete a genre by id.
*
* @param Genre $genre
*/
public function deleteObject($genre)
{
return $this->deleteById($genre->getId());
}
/**
* Soft delete a genre by id.
*
* @param int $genreId Genre ID
*/
public function deleteById($genreId)
{
return $this->update(
'UPDATE genres SET enabled = ? WHERE genre_id = ?',
[0, (int) $genreId]
);
}
/**
* Delete the genre entries associated with a context.
* Called when deleting a Context in ContextDAO.
*
* @param int $contextId Context ID
*/
public function deleteByContextId($contextId)
{
$genres = $this->getByContextId($contextId);
while ($genre = $genres->next()) {
$this->update('DELETE FROM genre_settings WHERE genre_id = ?', [(int) $genre->getId()]);
}
$this->update(
'DELETE FROM genres WHERE context_id = ?',
[(int) $contextId]
);
}
/**
* Install default data for settings.
*
* @param int $contextId Context ID
* @param array $locales List of locale codes
*/
public function installDefaults($contextId, $locales)
{
$xmlDao = new XMLDAO();
$data = $xmlDao->parseStruct('registry/genres.xml', ['genre']);
if (!isset($data['genre'])) {
return false;
}
$seq = 0;
foreach ($data['genre'] as $entry) {
$attrs = $entry['attributes'];
// attempt to retrieve an installed Genre with this key.
// Do this to preserve the genreId.
$genre = $this->getByKey($attrs['key'], $contextId);
if (!$genre) {
$genre = $this->newDataObject();
}
$genre->setContextId($contextId);
$genre->setKey($attrs['key']);
$genre->setCategory($attrs['category']);
$genre->setDependent($attrs['dependent']);
$genre->setSupplementary($attrs['supplementary']);
$genre->setRequired((bool) ($attrs['required'] ?? false));
$genre->setSequence($seq++);
foreach ($locales as $locale) {
$genre->setName(__($attrs['localeKey'], [], $locale), $locale);
}
if ($genre->getId() > 0) { // existing genre.
$genre->setEnabled(1);
$this->updateObject($genre);
} else {
$this->insertObject($genre);
}
}
}
/**
* Get default keys.
*
* @return array List of default keys
*/
public function getDefaultKeys()
{
$defaultKeys = [];
$xmlDao = new XMLDAO();
$data = $xmlDao->parseStruct('registry/genres.xml', ['genre']);
if (isset($data['genre'])) {
foreach ($data['genre'] as $entry) {
$attrs = $entry['attributes'];
$defaultKeys[] = $attrs['key'];
}
}
return $defaultKeys;
}
/**
* If a key exists for a context.
*
* @param string $key
* @param int $contextId
* @param int $genreId (optional) Current genre to be ignored
*
* @return bool
*/
public function keyExists($key, $contextId, $genreId = null)
{
$params = [$key, (int) $contextId];
if ($genreId) {
$params[] = (int) $genreId;
}
$result = $this->retrieveRange(
'SELECT COUNT(*) AS row_count FROM genres WHERE entry_key = ? AND context_id = ?' . (isset($genreId) ? ' AND genre_id <> ?' : ''),
$params
);
$row = $result->current();
return $row ? (bool) $row->row_count : false;
}
/**
* Remove all settings associated with a locale
*
* @param string $locale Locale code
*/
public function deleteSettingsByLocale($locale)
{
$this->update('DELETE FROM genre_settings WHERE locale = ?', [$locale]);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\GenreDAO', '\GenreDAO');
}
File diff suppressed because it is too large Load Diff
+906
View File
@@ -0,0 +1,906 @@
<?php
/**
* @file classes/submission/Repository.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 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 submissions.
*/
namespace PKP\submission;
use APP\author\Author;
use APP\core\Application;
use APP\core\Request;
use APP\core\Services;
use APP\facades\Repo;
use APP\publication\Publication;
use APP\section\Section;
use APP\submission\Collector;
use APP\submission\DAO;
use APP\submission\Submission;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use PKP\context\Context;
use PKP\core\Core;
use PKP\db\DAORegistry;
use PKP\doi\exceptions\DoiException;
use PKP\facades\Locale;
use PKP\observers\events\SubmissionSubmitted;
use PKP\plugins\Hook;
use PKP\query\QueryDAO;
use PKP\security\Role;
use PKP\security\RoleDAO;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submissionFile\SubmissionFile;
use PKP\user\User;
use PKP\validation\ValidatorFactory;
abstract class Repository
{
public const STAGE_STATUS_SUBMISSION_UNASSIGNED = 1;
/** @var DAO $dao */
public $dao;
/** @var string $schemaMap The name of the class to map this entity to its schema */
public $schemaMap = maps\Schema::class;
/** @var Request $request */
protected $request;
/** @var PKPSchemaService<Submission> $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 = []): Submission
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::exists() */
public function exists(int $id, int $contextId = null): bool
{
return $this->dao->exists($id, $contextId);
}
/** @copydoc DAO::get() */
public function get(int $id, int $contextId = null): ?Submission
{
return $this->dao->get($id, $contextId);
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return app(Collector::class);
}
/**
* Get an instance of the map class for mapping
* submissions to their schema
*/
public function getSchemaMap(): maps\Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Get a submission by "best" submission id -- url path if it exists,
* falling back on the internal submission ID otherwise.
*/
public function getByBestId(string $idOrUrlPath, int $contextId = null): ?Submission
{
return ctype_digit((string) $idOrUrlPath)
? $this->get((int) $idOrUrlPath, $contextId)
: $this->getByUrlPath($idOrUrlPath, $contextId);
}
/**
* Get a submission by its urlPath
*
* This returns a submission if any of its publications have a
* matching urlPath.
*/
public function getByUrlPath(string $urlPath, int $contextId): ?Submission
{
$submissionId = $this->dao->getIdByUrlPath($urlPath, $contextId);
return $submissionId
? $this->get($submissionId)
: null;
}
/**
* Gets a submission by its current publication's DOI
*
*
*/
public function getByDoi(string $doi, int $contextId): ?Submission
{
return $this->dao->getByDoi($doi, $contextId);
}
/** @copydoc DAO::getIdsBySetting() */
public function getIdsBySetting(string $settingName, $settingValue, int $contextId): Enumerable
{
return $this->dao->getIdsBySetting($settingName, $settingValue, $contextId);
}
/**
* Get the correct access URL for a submission's workflow based on a user's
* role.
*
* The returned URL will point to the correct workflow page based on whether
* the user should be treated as an author, reviewer or editor/assistant for
* this submission.
*/
public function getWorkflowUrlByUserRoles(Submission $submission, ?int $userId = null): string
{
$request = Application::get()->getRequest();
if (is_null($userId)) {
$user = $request->getUser();
} else {
$user = Repo::user()->get($userId);
}
if (is_null($user)) {
return '';
}
$submissionContext = $request->getContext();
if (!$submissionContext || $submissionContext->getId() != $submission->getData('contextId')) {
$submissionContext = Services::get('context')->get($submission->getData('contextId'));
}
$dispatcher = $request->getDispatcher();
// Check if the user is an author of this submission
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$authorUserGroupIds = Repo::userGroup()->getArrayIdByRoleId(Role::ROLE_ID_AUTHOR);
$stageAssignmentsFactory = $stageAssignmentDao->getBySubmissionAndStageId($submission->getId(), null, null, $user->getId());
$authorDashboard = false;
while ($stageAssignment = $stageAssignmentsFactory->next()) {
if (in_array($stageAssignment->getUserGroupId(), $authorUserGroupIds)) {
$authorDashboard = true;
}
}
// Send authors, journal managers and site admins to the submission
// wizard for incomplete submissions
if ($submission->getSubmissionProgress() &&
($authorDashboard ||
$user->hasRole([Role::ROLE_ID_MANAGER], $submissionContext->getId()) ||
$user->hasRole([Role::ROLE_ID_SITE_ADMIN], Application::CONTEXT_SITE))) {
return $dispatcher->url(
$request,
Application::ROUTE_PAGE,
$submissionContext->getPath(),
'submission',
null,
null,
['id' => $submission->getId()]
);
}
// Send authors to author dashboard
if ($authorDashboard) {
return $dispatcher->url(
$request,
Application::ROUTE_PAGE,
$submissionContext->getPath(),
'authorDashboard',
'submission',
$submission->getId()
);
}
// Send reviewers to review wizard
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignment = $reviewAssignmentDao->getLastReviewRoundReviewAssignmentByReviewer($submission->getId(), $user->getId());
if ($reviewAssignment && !$reviewAssignment->getCancelled() && !$reviewAssignment->getDeclined()) {
return $dispatcher->url(
$request,
Application::ROUTE_PAGE,
$submissionContext->getPath(),
'reviewer',
'submission',
$submission->getId()
);
}
// Give any other users the editorial workflow URL. If they can't access
// it, they'll be blocked there.
return $dispatcher->url(
$request,
Application::ROUTE_PAGE,
$submissionContext->getPath(),
'workflow',
'access',
$submission->getId()
);
}
/**
* Validate properties for a submission
*
* Perform validation checks on data used to add or edit a submission.
*
* @param Submission|null $submission The submission being edited. Pass `null` if creating a new submission
* @param array $props A key/value array with the new data to validate
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate(?Submission $submission, array $props, Context $context): array
{
$primaryLocale = $props['locale'] ?? $submission?->getLocale() ?? $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedSubmissionLocales();
$errors = [];
$validator = ValidatorFactory::make(
$props,
$this->schemaService->getValidationRules(PKPSchemaService::SCHEMA_SUBMISSION, $allowedLocales)
);
// Check required fields
ValidatorFactory::required(
$validator,
$submission,
$this->schemaService->getRequiredProps(PKPSchemaService::SCHEMA_SUBMISSION),
$this->schemaService->getMultilingualProps(PKPSchemaService::SCHEMA_SUBMISSION),
$primaryLocale,
$allowedLocales
);
// Check for input from disallowed locales
ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps(PKPSchemaService::SCHEMA_SUBMISSION), $allowedLocales);
// The submission's locale must be one of the context's supported submission locales
$validator->after(function ($validator) use ($props, $allowedLocales) {
if (isset($props['locale']) && !$validator->errors()->get('locale')) {
if (!in_array($props['locale'], $allowedLocales)) {
$validator->errors()->add('locale', __('validator.locale'));
}
}
});
// The contextId must match an existing context
$validator->after(function ($validator) use ($props) {
if (isset($props['contextId']) && !$validator->errors()->get('contextId')) {
$submissionContext = Services::get('context')->exists($props['contextId']);
if (!$submissionContext) {
$validator->errors()->add('contextId', __('submission.submit.noContext'));
}
}
});
// The sectionId must match an existing section in this context
$validator->after(function ($validator) use ($props, $submission) {
$propName = Application::getSectionIdPropName();
if ($validator->errors()->get($propName)) {
return;
}
$sectionId = $props[$propName] ?? ($submission ? $submission->getCurrentPublication()->getData($propName) : null);
if (!$sectionId) {
return;
}
$contextId = $props['contextId'] ?? ($submission ? $submission->getData('contextId') : null);
if (!Repo::section()->exists($sectionId, $contextId)) {
$validator->errors()->add($propName, __('submission.sectionNotFound'));
return;
}
});
// Comments for the editors are invalid after a submission has been submitted
if ($submission && !$submission->getData('submissionProgress')) {
$validator->after(function ($validator) use ($props, $submission) {
if (isset($props['commentsForTheEditors']) && !$validator->errors()->get('commentsForTheEditors')) {
$validator->errors()->add('commentsForTheEditors', __('form.disallowedProp'));
}
});
}
if ($validator->fails()) {
$errors = $this->schemaService->formatValidationErrors($validator->errors());
}
Hook::call('Submission::validate', [&$errors, $submission, $props, $allowedLocales, $primaryLocale]);
return $errors;
}
/**
* Check if a submission meets all requirements to be submitted
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validateSubmit(Submission $submission, Context $context): array
{
$locale = $submission->getData('locale');
$publication = $submission->getCurrentPublication();
$errors = [];
// Can't submit a submission twice or a submission with the wrong status
if (!$submission->getData('submissionProgress') || ($submission->getData('status') !== Submission::STATUS_QUEUED)) {
$errors['submissionProgress'] = __(
'submission.wizard.alreadySubmitted',
[
'url' => Application::get()
->getDispatcher()
->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$context->getData('path'),
'submissions'
)
]
);
}
// Title required in submission locale
if (!$publication->getData('title', $locale)) {
$errors['title'] = [$locale => [__('validator.required')]];
}
// Author names required in submission locale
foreach ($publication->getData('authors') as $author) {
/** @var Author $author */
if (!$author->getGivenName($submission->getLocale())) {
if (!isset($errors['contributors'])) {
$errors['contributors'] = [];
}
$errors['contributors'][] = __('submission.wizard.missingContributorLanguage', ['language' => Locale::getMetadata($locale)->getDisplayName()]);
break;
}
}
// Required metadata
$publicationSchema = $this->schemaService->get(PKPSchemaService::SCHEMA_PUBLICATION);
foreach ($context->getRequiredMetadata() as $metadata) {
// The `citations` metadata is received and validated at `citationsRaw`
if ($metadata === 'citations') {
$metadata = 'citationsRaw';
}
// The `supportingAgencies` metadata is called `agencies` on the context
if ($metadata === 'agencies') {
$metadata = 'supportingAgencies';
}
$schema = $publicationSchema->properties?->{$metadata};
if (!$schema) {
continue;
}
if (empty($schema->multilingual) && empty($publication->getData($metadata))) {
$errors[$metadata] = [__('validator.required')];
} elseif (!empty($schema->multilingual) && empty($publication->getData($metadata, $locale))) {
$errors[$metadata] = [$locale => [__('validator.required')]];
}
}
// Required submission files
$genreDao = DAORegistry::getDAO('GenreDAO'); /** @var GenreDAO $genreDao */
$requiredGenres = $genreDao->getRequiredToSubmit($context->getId());
if (!$requiredGenres->isEmpty()) {
$submissionFiles = Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByGenreIds(
$requiredGenres->map(
function (Genre $genre) {
return $genre->getId();
}
)->toArray()
)
->getMany();
$missingGenres = $submissionFiles->isEmpty()
? clone $requiredGenres
: $requiredGenres->filter(
function (Genre $genre) use ($submissionFiles) {
$exists = $submissionFiles->first(
function (SubmissionFile $submissionFile) use ($genre) {
return $submissionFile->getData('genreId') === $genre->getId();
}
);
return !$exists;
}
);
if ($missingGenres->count()) {
$missingGenreNames = $missingGenres->map(
function (Genre $genre) {
return $genre->getLocalizedName();
}
);
$errors['files'] = [
$missingGenres->count() > 1
? __('submission.files.required.genres', [
'genres' => $missingGenreNames->join(__('common.commaListSeparator'))
])
: __('submission.files.required.genre', ['genre' => $missingGenreNames->first()])
];
}
}
Hook::call('Submission::validateSubmit', [&$errors, $submission, $context]);
return $errors;
}
/**
* Check if a user can delete a submission
*/
public function canCurrentUserDelete(Submission $submission): bool
{
$this->request = Application::get()->getRequest();
$contextId = $submission->getData('contextId');
$currentUser = $this->request->getUser();
if (!$currentUser) {
return false;
}
$canDelete = false;
// Only allow admins and journal managers to delete submissions, except
// for authors who can delete their own incomplete submissions
if ($currentUser->hasRole([Role::ROLE_ID_MANAGER], $contextId) || $currentUser->hasRole([Role::ROLE_ID_SITE_ADMIN], Application::CONTEXT_SITE)) {
$canDelete = true;
} else {
if ($submission->getData('submissionProgress')) {
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$assignments = $stageAssignmentDao->getBySubmissionAndRoleIds($submission->getId(), [Role::ROLE_ID_AUTHOR], WORKFLOW_STAGE_ID_SUBMISSION, $currentUser->getId());
$assignment = $assignments->next();
if ($assignment) {
$canDelete = true;
}
}
}
return $canDelete;
}
/**
* Check if a user can edit the publication metadata of a submission
*/
public function canEditPublication(int $submissionId, int $userId): bool
{
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignments = $stageAssignmentDao->getBySubmissionAndUserIdAndStageId($submissionId, $userId, null)->toArray();
// Check for permission from stage assignments
foreach ($stageAssignments as $stageAssignment) {
if ($stageAssignment->getCanChangeMetadata()) {
return true;
}
}
// If user has no stage assigments, check if user can edit anyway ie. is manager
$context = Application::get()->getRequest()->getContext();
if (count($stageAssignments) == 0 && $this->_canUserAccessUnassignedSubmissions($context->getId(), $userId)) {
return true;
}
// Else deny access
return false;
}
/**
* Checks if this user is granted reader access to pre-publication submissions
* based on their roles in the context (i.e. Manager, Editor, etc).
*/
public function canPreview(?User $user, Submission $submission): bool
{
// Only grant access when in copyediting or production stage
if (!in_array($submission->getData('stageId'), [WORKFLOW_STAGE_ID_EDITING, WORKFLOW_STAGE_ID_PRODUCTION])) {
return false;
}
if ($this->_roleCanPreview($user, $submission)) {
return true;
}
if ($user) {
/** @var StageAssignmentDAO */
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
$stageAssignments = $stageAssignmentDao->getBySubmissionAndRoleId($submission->getId(), Role::ROLE_ID_AUTHOR, null, $user->getId());
$stageAssignment = $stageAssignments->next();
if ($stageAssignment) {
return true;
}
}
return false;
}
/**
* Add a new submission
*/
public function add(Submission $submission, Publication $publication, Context $context): int
{
$submission->stampLastActivity();
$submission->stampModified();
if (!$submission->getData('dateSubmitted') && !$submission->getData('submissionProgress')) {
$submission->setData('dateSubmitted', Core::getCurrentDate());
}
if (!$submission->getData('status')) {
$submission->setData('status', Submission::STATUS_QUEUED);
}
if (!$submission->getData('locale')) {
$submission->setData('locale', $context->getPrimaryLocale());
}
$submissionId = $this->dao->insert($submission);
$submission = Repo::submission()->get($submissionId);
$publication->setData('submissionId', $submission->getId());
$publication->setData('version', 1);
if (!$publication->getData('status')) {
$publication->setData('status', $submission->getData('status'));
}
$publicationId = Repo::publication()->add($publication);
$this->edit($submission, ['currentPublicationId' => $publicationId]);
Hook::call('Submission::add', [$submission]);
return $submission->getId();
}
/** @copydoc DAO::update */
public function edit(Submission $submission, array $params)
{
$newSubmission = Repo::submission()->newDataObject(array_merge($submission->_data, $params));
$newSubmission->stampLastActivity();
$newSubmission->stampModified();
Hook::call('Submission::edit', [$newSubmission, $submission, $params]);
$this->dao->update($newSubmission);
}
/**
* Submit a submission
*
* Changes the submissionProgress property, creates the comments
* for the editors discussion, and fires the SubmissionSubmitted
* event.
*/
public function submit(Submission $submission, Context $context): void
{
$this->edit($submission, [
'submissionProgress' => '',
'dateSubmitted' => Core::getCurrentDate(),
]);
$submission = $this->get($submission->getId());
event(
new SubmissionSubmitted(
$submission,
$context
)
);
if ($submission->getData('commentsForTheEditors')) {
/** @var QueryDAO $queryDao */
$queryDao = DAORegistry::getDAO('QueryDAO');
$queryDao->addCommentsForEditorsQuery($submission);
}
}
/** @copydoc DAO::delete */
public function delete(Submission $submission)
{
Hook::call('Submission::delete::before', [&$submission]);
$this->dao->delete($submission);
Hook::call('Submission::delete', [$submission]);
}
/**
* Delete all submissions in a context
*/
public function deleteByContextId(int $contextId)
{
$submissionIds = Repo::submission()->getCollector()->filterByContextIds([$contextId])->getIds();
foreach ($submissionIds as $submissionId) {
$this->dao->deleteById($submissionId);
}
}
/**
* Update a submission's status
*
* Changes a submission's status. Or, if no new status is provided,
* sets the appropriate status based on all of the submission's
* publications.
*
* This method performs any actions necessary when a submission's
* status changes, such as changing the current publication ID
* and creating or deleting tombstones.
*
* @param ?Section $section If this submission is being deleted, its previous section ID should be specified
* in order to ensure a correctly created tombstone.
*/
public function updateStatus(Submission $submission, ?int $newStatus = null, ?Section $section = null)
{
$status = $submission->getData('status');
if ($newStatus === null) {
$newStatus = $this->getStatusByPublications($submission);
}
Hook::call('Submission::updateStatus', [&$newStatus, $status, $submission]);
if ($status !== $newStatus) {
$submission->setData('status', $newStatus);
}
$currentPublicationId = $newCurrentPublicationId = $submission->getData('currentPublicationId');
$newCurrentPublicationId = $this->getCurrentPublicationIdByPublications($submission);
if ($currentPublicationId !== $newCurrentPublicationId) {
$submission->setData('currentPublicationId', $newCurrentPublicationId);
}
// Use the DAO instead of the Repository to prevent
// calling this method over and over again.
$this->dao->update($submission);
}
/**
* Set license information for all submissions in a context
* to the context's default license.
*/
public function resetPermissions(int $contextId)
{
$submissions = Repo::submission()->getCollector()->filterByContextIds([$contextId])->getMany();
foreach ($submissions as $submission) {
$publications = $submission->getData('publications');
if (empty($publications)) {
continue;
}
$params = [
'copyrightYear' => $submission->_getContextLicenseFieldValue(null, Submission::PERMISSIONS_FIELD_COPYRIGHT_YEAR),
'copyrightHolder' => $submission->_getContextLicenseFieldValue(null, Submission::PERMISSIONS_FIELD_COPYRIGHT_HOLDER),
'licenseUrl' => $submission->_getContextLicenseFieldValue(null, Submission::PERMISSIONS_FIELD_LICENSE_URL),
];
foreach ($publications as $publication) {
Repo::publication()->edit($publication, $params);
}
}
}
/**
* Get an array of sort options used in forms when configuring
* how published submissions are displayed
*/
public function getSortSelectOptions(): array
{
return [
$this->getSortOption(Collector::ORDERBY_TITLE, Collector::ORDER_DIR_ASC) => __('catalog.sortBy.titleAsc'),
$this->getSortOption(Collector::ORDERBY_TITLE, Collector::ORDER_DIR_DESC) => __('catalog.sortBy.titleDesc'),
$this->getSortOption(Collector::ORDERBY_DATE_PUBLISHED, Collector::ORDER_DIR_ASC) => __('catalog.sortBy.datePublishedAsc'),
$this->getSortOption(Collector::ORDERBY_DATE_PUBLISHED, Collector::ORDER_DIR_DESC) => __('catalog.sortBy.datePublishedDesc'),
];
}
/**
* Get the default sort option used in forms when configuring
* how published submissions are displayed
*
* @see self::getSortSelectOptions()
*/
public function getDefaultSortOption(): string
{
return $this->getSortOption(Collector::ORDERBY_DATE_PUBLISHED, Collector::ORDER_DIR_DESC);
}
/**
* Get the URL to the API endpoint for a submission
*/
public function getUrlApi(Context $context, ?int $submissionId = null): string
{
return Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_API,
$context->getData('urlPath'),
'submissions' . ($submissionId ? '/' . $submissionId : ''),
);
}
/**
* Get the URL to the author workflow for a submission
*/
public function getUrlAuthorWorkflow(Context $context, int $submissionId): string
{
return Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$context->getData('urlPath'),
'authorDashboard',
'submission',
$submissionId
);
}
/**
* Get the URL to the editorial workflow for a submission
*/
public function getUrlEditorialWorkflow(Context $context, int $submissionId): string
{
return Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$context->getData('urlPath'),
'workflow',
'access',
$submissionId
);
}
/**
* Get the URL to the submission wizard for a submission
*/
public function getUrlSubmissionWizard(Context $context, ?int $submissionId = null): string
{
return Application::get()->getDispatcher()->url(
Application::get()->getRequest(),
Application::ROUTE_PAGE,
$context->getData('urlPath'),
'submission',
null,
null,
$submissionId
? ['id' => $submissionId]
: null
);
}
/**
* Creates and assigns DOIs to all sub-objects if:
* 1) the suffix pattern can currently be created, and
* 2) it does not already exist.
*
* @return DoiException[]
*/
abstract public function createDois(Submission $submission): array;
/**
* Compile the sort orderBy and orderDirection into an option
* used in forms
*/
protected function getSortOption(string $sortBy, string $sortDir): string
{
return $sortBy . '-' . $sortDir;
}
/**
* Check if a user is allowed to edit publication metadata for submissions
* they are not assigned to
*/
protected function _canUserAccessUnassignedSubmissions(int $contextId, int $userId): bool
{
$roleDao = DAORegistry::getDAO('RoleDAO'); /** @var RoleDAO $roleDao */
$roles = $roleDao->getByUserId($userId, $contextId);
$allowedRoles = Repo::userGroup()::NOT_CHANGE_METADATA_EDIT_PERMISSION_ROLES;
foreach ($roles as $role) {
if (in_array($role->getRoleId(), $allowedRoles)) {
return true;
}
}
return false;
}
/**
* Get the appropriate status of a submission based on the
* statuses of its publications
*/
protected function getStatusByPublications(Submission $submission): int
{
$publications = $submission->getData('publications'); /** @var LazyCollection $publications */
// Declined submissions should remain declined regardless of their publications' statuses
if ($submission->getData('status') === Submission::STATUS_DECLINED) {
return Submission::STATUS_DECLINED;
}
// If there are no publications, we are probably in the process of deleting a submission.
// To be safe, reset the status anyway.
if (!$publications->count()) {
return Submission::STATUS_DECLINED
? Submission::STATUS_DECLINED
: Submission::STATUS_QUEUED;
}
$newStatus = Submission::STATUS_QUEUED;
foreach ($publications as $publication) {
if ($publication->getData('status') === Submission::STATUS_PUBLISHED) {
$newStatus = Submission::STATUS_PUBLISHED;
break;
}
if ($publication->getData('status') === Submission::STATUS_SCHEDULED) {
$newStatus = Submission::STATUS_SCHEDULED;
continue;
}
}
return $newStatus;
}
/**
* Get the appropriate currentPublicationId for a submission based on the
* statues of its publications
*/
protected function getCurrentPublicationIdByPublications(Submission $submission): ?int
{
$publications = $submission->getData('publications'); /** @var LazyCollection $publications */
if (!$publications->count()) {
return null;
}
// Use the latest published publication
$newCurrentPublicationId = $publications->reduce(function ($a, $b) {
return $b->getData('status') === Submission::STATUS_PUBLISHED && $b->getId() > $a ? $b->getId() : $a;
}, 0);
// If there is no published publication, use the latest publication
if (!$newCurrentPublicationId) {
$newCurrentPublicationId = $publications->reduce(function ($a, $b) {
return $a > $b->getId() ? $a : $b->getId();
}, 0);
}
return $newCurrentPublicationId ?? $submission->getData('currentPublicationId');
}
/**
* Checks if this user is granted access to preview
* based on their roles in the context (i.e. Manager, Editor, etc).
*
* @param User $user
*
*/
protected function _roleCanPreview(?User $user, Submission $submission): bool
{
if (!$user) {
return false;
}
$subscriptionAssumedRoles = [
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_ASSISTANT,
Role::ROLE_ID_SUBSCRIPTION_MANAGER
];
/** @var RoleDAO */
$roleDao = DAORegistry::getDAO('RoleDAO');
$roles = $roleDao->getByUserId($user->getId(), $submission->getData('contextId'));
foreach ($roles as $role) {
if (in_array($role->getRoleId(), $subscriptionAssumedRoles)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,207 @@
<?php
/**
* @file classes/submission/Representation.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Representation
*
* @ingroup submission
*
* @brief A submission's representation (Publication Format, Galley, ...)
*/
namespace PKP\submission;
use APP\core\Application;
use APP\facades\Repo;
/**
* @extends \PKP\core\DataObject<DAO|RepresentationDAOInterface>
*/
class Representation extends \PKP\core\DataObject
{
/**
* Constructor.
*/
public function __construct()
{
// Switch on meta-data adapter support.
$this->setHasLoadableAdapters(true);
parent::__construct();
}
/**
* Get sequence of format in format listings for the submission.
*
* @return float
*/
public function getSequence()
{
return $this->getData('seq');
}
/**
* Set sequence of format in format listings for the submission.
*
* @param float $seq
*/
public function setSequence($seq)
{
$this->setData('seq', $seq);
}
/**
* Get "localized" format name (if applicable).
*
* @return string
*/
public function getLocalizedName()
{
return $this->getLocalizedData('name');
}
/**
* Get the format name (if applicable).
*
* @param ?string $locale
*
* @return string|array<string,string>
*/
public function getName($locale = null)
{
return $this->getData('name', $locale);
}
/**
* Set name.
*
* @param string $name
* @param string $locale
*/
public function setName($name, $locale = null)
{
$this->setData('name', $name, $locale);
}
/**
* Determines if a representation is approved or not.
*
* @return bool
*/
public function getIsApproved()
{
return (bool) $this->getData('isApproved');
}
/**
* Sets whether a representation is approved or not.
*
* @param bool $isApproved
*/
public function setIsApproved($isApproved)
{
return $this->setData('isApproved', $isApproved);
}
/**
* Returns current DOI
*
*/
public function getDoi(): ?string
{
$doiObject = $this->getData('doiObject');
if (empty($doiObject)) {
return null;
} else {
return $doiObject->getData('doi');
}
}
/**
* Get stored public ID of the submission.
*
* This helper function is required by PKPPubIdPlugins.
* NB: To maintain backwards compatibility, getDoi() is called from here
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
*
* @return string
*/
public function getStoredPubId($pubIdType)
{
if ($pubIdType == 'doi') {
return $this->getDoi();
} else {
return $this->getData('pub-id::' . $pubIdType);
}
}
/**
* Set the stored public ID of the submission.
*
* @param string $pubIdType One of the NLM pub-id-type values or
* 'other::something' if not part of the official NLM list
* (see <http://dtd.nlm.nih.gov/publishing/tag-library/n-4zh0.html>).
* @param string $pubId
*/
public function setStoredPubId($pubIdType, $pubId)
{
$this->setData('pub-id::' . $pubIdType, $pubId);
}
/**
* Get the remote URL at which this representation is retrievable.
*
* @return string
*
* @deprecated 3.2.0.0
*/
public function getRemoteURL()
{
return $this->getData('urlRemote');
}
/**
* Set the remote URL for retrieving this representation.
*
* @param string $remoteURL
*
* @deprecated 3.2.0.0
*/
public function setRemoteURL($remoteURL)
{
return $this->setData('urlRemote', $remoteURL);
}
/**
* Get the context id from the submission assigned to this representation.
*
* @return int
*/
public function getContextId()
{
$publication = Repo::publication()->get($this->getData('publicationId'));
$submission = Repo::submission()->get($publication->getData('submissionId'));
return $submission->getContextId();
}
/**
* @copydoc \PKP\core\DataObject::getDAO()
*/
public function getDAO()
{
return Application::getRepresentationDAO();
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\Representation', '\Representation');
}
@@ -0,0 +1,39 @@
<?php
/**
* @file classes/galley/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class galley
*
* @brief An interface for representation DAOs (galleys and publication formats)
*/
namespace PKP\submission;
use PKP\plugins\PKPPubIdPluginDAO;
interface RepresentationDAOInterface extends PKPPubIdPluginDAO
{
/**
* Instantiate a new Representation object
*/
public function newDataObject(): Representation;
/**
* Get a representation by id
*/
public function getById(int $id, ?int $publicationId = null, ?int $contextId = null): ?Representation;
/**
* Get the representations of a publication
*/
public function getByPublicationId(int $publicationId): array;
/**
* Update the representation object
*/
public function updateObject(Representation $representation): void;
}
@@ -0,0 +1,98 @@
<?php
/**
* @file classes/submission/ReviewFilesDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewFilesDAO
*
* @ingroup submission
*
* @brief Operations for managing review round / submission file associations.
* These control which files are available for download by reviewers during review.
*/
namespace PKP\submission;
use Illuminate\Support\Facades\DB;
class ReviewFilesDAO extends \PKP\db\DAO
{
/**
* Grant a review file to a review.
*
* @param int $reviewId Review assignment ID
* @param int $submissionFileId Submission file ID
*/
public function grant($reviewId, $submissionFileId)
{
$this->update(
'INSERT INTO review_files
(review_id, submission_file_id)
VALUES
(?, ?)',
[(int) $reviewId, (int) $submissionFileId]
);
}
/**
* Revoke a review's association with a review file.
*
* @param int $reviewId Review assignment ID.
* @param int $fileId Review file ID.
*/
public function revoke($reviewId, $fileId)
{
$this->update(
'DELETE FROM review_files WHERE review_id = ? AND file_id = ?',
[(int) $reviewId, (int) $fileId]
);
}
/**
* Revoke a review's association with all submission files.
*
* @param int $reviewId Review assignment ID.
*/
public function revokeByReviewId($reviewId)
{
$this->update(
'DELETE FROM review_files WHERE review_id = ?',
[(int) $reviewId]
);
}
/**
* Revoke a review's association based on submission file id.
*/
public function revokeBySubmissionFileId(int $submissionFileId)
{
$this->update(
'DELETE FROM review_files WHERE submission_file_id = ?',
[(int) $submissionFileId]
);
}
/**
* Check review file availability
*
* @param int $reviewId
* @param int $submissionFileId
*
* @return bool
*/
public function check($reviewId, $submissionFileId)
{
return DB::table('review_files')
->where('review_id', (int) $reviewId)
->where('submission_file_id', (int) $submissionFileId)
->exists();
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\ReviewFilesDAO', '\ReviewFilesDAO');
}
+162
View File
@@ -0,0 +1,162 @@
<?php
/**
* @file classes/submission/Sanitizer.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Sanitizer
*
* @brief A sanitization class to sanitize submission data before saving
*/
namespace PKP\submission;
use Exception;
use Illuminate\Support\Arr;
use PKP\core\PKPString;
class Sanitizer
{
/**
* Defined sanitization rule/method mapping to attributes
* It's possible to have multiple sanitization rule for a single attributes
*
* $sanitizeMap = [
* 'attribute_1' => 'class_method_1',
* 'attribute_2' => ['class_method_21', 'class_method_22'],
* ...
* ]
*/
protected array $sanitizeMap = [];
/**
* Passed params to sanitize
*/
protected array $sanitizeParams;
/**
* Define if allow empty sanitization for attributes
*/
protected bool $allowEmptySanization = false;
/**
* The entity code to number conversion to update
* As TinyMCE do a force entity conversion even when defined the 'entity_encoding' as 'raw'
*
* @see 5.0+ : https://www.tiny.cloud/docs/configure/content-filtering/#entity_encoding
* @see 6.0+ : https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding
*/
protected static array $entityCodeToCharMapping = [
'&' => '&amp;',
'>' => '&gt;',
'<' => '&lt;',
'"' => '&quot;',
"'" => '&apos;',
];
/**
* Convert the TinyMCE/HTMLPurify based converted entity codes to actual character
*/
public static function replaceSpecialCharEntityValueWithCharacter(string $string): string
{
return str_replace(
array_values(static::$entityCodeToCharMapping),
array_keys(static::$entityCodeToCharMapping),
$string
);
}
/**
* Apply the sanitization process for given attribute
*/
protected function runSanitizationProcess(string $method, mixed $paramKey, mixed $beforeSanitizevalue): void
{
$this->sanitizeParams = array_merge($this->sanitizeParams, [
$paramKey => $this->{$method}($beforeSanitizevalue),
]);
}
/**
* Sanitize the submission localized/unlocalized title[title] attribute
*/
public function title(string|array $param): string|array
{
if (is_array($param)) {
foreach ($param as $localeKey => $localizedSubmissionTitle) {
// TinyMCE sometimes converts special chars to entity code and some times not
// A very weird quirk by tinyMCE
// e.g '&' turned into '&amp;'
$param[$localeKey] = self::replaceSpecialCharEntityValueWithCharacter(
PKPString::stripUnsafeHtml($localizedSubmissionTitle, 'allowed_title_html')
);
}
return $param;
}
return self::replaceSpecialCharEntityValueWithCharacter(
PKPString::stripUnsafeHtml($param, 'allowed_title_html')
);
}
/**
* Sanitize the submission localized/unlocalized sub title[subtitle] attribute
*/
public function subtitle(string|array $param): string|array
{
return $this->title($param);
}
/**
* Define should allow attributes with empty sanitization rules
*/
public function allowEmptySanitization(): self
{
$this->allowEmptySanization = true;
return $this;
}
/**
* Run sanitization
*/
public function sanitize(array $params, array $sanitizingKeys = []): array
{
$this->sanitizeParams = $params;
$sanitizableParams = empty($sanitizingKeys)
? $params
: array_intersect_key($params, array_flip($sanitizingKeys));
foreach ($sanitizableParams as $paramKey => $paramValue) {
if (in_array($paramKey, $this->sanitizeMap)) {
collect(Arr::wrap($this->sanitizeMap[$paramKey]))
->each(
fn ($method) => $this->runSanitizationProcess(
$method,
$paramKey,
$paramValue
)
);
continue;
}
if (method_exists($this, $paramKey)) {
$this->runSanitizationProcess($paramKey, $paramKey, $paramValue);
continue;
}
if (!$this->allowEmptySanization) {
throw new Exception(
sprintf("Running empty sanitization for attribute '%s' is now allowed", $paramKey)
);
}
}
return $this->sanitizeParams;
}
}
@@ -0,0 +1,56 @@
<?php
/**
* @file classes/submission/SubmissionAgency.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionAgency
*
* @ingroup submission
*
* @see SubmissionAgencyEntryDAO
*
* @brief Basic class describing a submission agency
*/
namespace PKP\submission;
class SubmissionAgency extends \PKP\controlledVocab\ControlledVocabEntry
{
//
// Get/set methods
//
/**
* Get the agency
*
* @return string
*/
public function getAgency()
{
return $this->getData('submissionAgency');
}
/**
* Set the agency text
*
* @param string $agency
* @param string $locale
*/
public function setAgency($agency, $locale)
{
$this->setData('submissionAgency', $agency, $locale);
}
public function getLocaleMetadataFieldNames()
{
return ['submissionAgency'];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionAgency', '\SubmissionAgency');
}
@@ -0,0 +1,141 @@
<?php
/**
* @file classes/submission/SubmissionAgencyDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionAgencyDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's assigned agencies
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocab;
use PKP\controlledVocab\ControlledVocabDAO;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
class SubmissionAgencyDAO extends ControlledVocabDAO
{
public const CONTROLLED_VOCAB_SUBMISSION_AGENCY = 'submissionAgency';
/**
* Build/fetch and return a controlled vocabulary for agencies.
*
* @param int $publicationId
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*
* @return ControlledVocab
*/
public function build($publicationId, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
return parent::_build(self::CONTROLLED_VOCAB_SUBMISSION_AGENCY, $assocType, $publicationId);
}
/**
* Get the list of localized additional fields to store.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['submissionAgency'];
}
/**
* Get agencies for a specified submission ID.
*
* @param int $publicationId
* @param array $locales
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#6213
*
* @return array
*/
public function getAgencies($publicationId, $locales = [], $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$result = [];
$agencies = $this->build($publicationId, $assocType);
$submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var SubmissionAgencyEntryDAO $submissionAgencyEntryDao */
$submissionAgencies = $submissionAgencyEntryDao->getByControlledVocabId($agencies->getId());
while ($agencyEntry = $submissionAgencies->next()) {
$agency = $agencyEntry->getAgency();
foreach ($agency as $locale => $value) {
if (empty($locales) || in_array($locale, $locales)) {
$result[$locale][] = $value;
}
}
}
return $result;
}
/**
* Get an array of all of the submission's agencies
*
* @return array
*/
public function getAllUniqueAgencies()
{
$result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_AGENCY]);
$agencies = [];
foreach ($result as $row) {
$agencies[] = $row->setting_value;
}
return $agencies;
}
/**
* Add an array of agencies
*
* @param array $agencies List of agencies.
* @param int $publicationId Submission ID.
* @param bool $deleteFirst True iff existing agencies should be removed first.
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*/
public function insertAgencies($agencies, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$agencyDao = DAORegistry::getDAO('SubmissionAgencyDAO'); /** @var SubmissionAgencyDAO $agencyDao */
$submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var SubmissionAgencyEntryDAO $submissionAgencyEntryDao */
$currentAgencies = $this->build($publicationId, $assocType);
if ($deleteFirst) {
$existingEntries = $agencyDao->enumerate($currentAgencies->getId(), self::CONTROLLED_VOCAB_SUBMISSION_AGENCY);
foreach ($existingEntries as $id => $entry) {
$entry = trim($entry);
$submissionAgencyEntryDao->deleteObjectById($id);
}
}
if (is_array($agencies)) { // localized, array of arrays
foreach ($agencies as $locale => $list) {
if (is_array($list)) {
$list = array_unique($list); // Remove any duplicate keywords
$i = 1;
foreach ($list as $agency) {
$agencyEntry = $submissionAgencyEntryDao->newDataObject();
$agencyEntry->setControlledVocabId($currentAgencies->getId());
$agencyEntry->setAgency($agency, $locale);
$agencyEntry->setSequence($i);
$i++;
$submissionAgencyEntryDao->insertObject($agencyEntry);
}
}
}
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionAgencyDAO', '\SubmissionAgencyDAO');
define('CONTROLLED_VOCAB_SUBMISSION_AGENCY', SubmissionAgencyDAO::CONTROLLED_VOCAB_SUBMISSION_AGENCY);
}
@@ -0,0 +1,61 @@
<?php
/**
* @file classes/submission/SubmissionAgencyEntryDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionAgencyEntryDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's agencies
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocabEntryDAO;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
class SubmissionAgencyEntryDAO extends ControlledVocabEntryDAO
{
/**
* Construct a new data object corresponding to this DAO.
*
* @return SubmissionAgency
*/
public function newDataObject()
{
return new SubmissionAgency();
}
/**
* Retrieve an iterator of controlled vocabulary entries matching a
* particular controlled vocabulary ID.
*
* @param int $controlledVocabId
* @param mixed $filter (Not yet supported)
* @param ?DBResultRange $rangeInfo
*
* @return DAOResultFactory<SubmissionAgency> Object containing matching CVE objects
*/
public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null)
{
assert($filter == null); // Parent class supports this, but this class does not
$result = $this->retrieveRange(
'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq',
[(int) $controlledVocabId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionAgencyEntryDAO', '\SubmissionAgencyEntryDAO');
}
@@ -0,0 +1,273 @@
<?php
/**
* @file classes/submission/SubmissionComment.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionComment
*
* @ingroup submission
*
* @see SubmissionCommentDAO
*
* @brief Class for SubmissionComment.
*/
namespace PKP\submission;
use APP\facades\Repo;
class SubmissionComment extends \PKP\core\DataObject
{
public const COMMENT_TYPE_PEER_REVIEW = 1;
public const COMMENT_TYPE_EDITOR_DECISION = 2;
public const COMMENT_TYPE_COPYEDIT = 3;
public const COMMENT_TYPE_LAYOUT = 4;
public const COMMENT_TYPE_PROOFREAD = 5;
/**
* get comment type
*
* @return int COMMENT_TYPE_...
*/
public function getCommentType()
{
return $this->getData('commentType');
}
/**
* set comment type
*
* @param int $commentType COMMENT_TYPE_...
*/
public function setCommentType($commentType)
{
$this->setData('commentType', $commentType);
}
/**
* get role id
*
* @return int
*/
public function getRoleId()
{
return $this->getData('roleId');
}
/**
* set role id
*
* @param int $roleId
*/
public function setRoleId($roleId)
{
$this->setData('roleId', $roleId);
}
/**
* get submission id
*
* @return int
*/
public function getSubmissionId()
{
return $this->getData('submissionId');
}
/**
* set submission id
*
* @param int $submissionId
*/
public function setSubmissionId($submissionId)
{
$this->setData('submissionId', $submissionId);
}
/**
* get assoc id
*
* @return int
*/
public function getAssocId()
{
return $this->getData('assocId');
}
/**
* set assoc id
*
* @param int $assocId
*/
public function setAssocId($assocId)
{
$this->setData('assocId', $assocId);
}
/**
* get author id
*
* @return int
*/
public function getAuthorId()
{
return $this->getData('authorId');
}
/**
* set author id
*
* @param int $authorId
*/
public function setAuthorId($authorId)
{
$this->setData('authorId', $authorId);
}
/**
* get author name
*
* @return string
*/
public function getAuthorName()
{
// Reference used to set if not already fetched
$authorFullName = & $this->getData('authorFullName');
if (!isset($authorFullName)) {
$user = Repo::user()->get($this->getAuthorId(), true);
$authorFullName = $user->getFullName();
}
return $authorFullName ? $authorFullName : '';
}
/**
* get author email
*
* @return string
*/
public function getAuthorEmail()
{
// Reference used to set if not already fetched
$authorEmail = & $this->getData('authorEmail');
if (!isset($authorEmail)) {
$user = Repo::user()->get($this->getAuthorId(), true);
return $user->getEmail();
}
return $authorEmail ? $authorEmail : '';
}
/**
* get comment title
*
* @return string
*/
public function getCommentTitle()
{
return $this->getData('commentTitle');
}
/**
* set comment title
*
* @param string $commentTitle
*/
public function setCommentTitle($commentTitle)
{
$this->setData('commentTitle', $commentTitle);
}
/**
* get comments
*
* @return string
*/
public function getComments()
{
return $this->getData('comments');
}
/**
* set comments
*
* @param string $comments
*/
public function setComments($comments)
{
$this->setData('comments', $comments);
}
/**
* get date posted
*
* @return string
*/
public function getDatePosted()
{
return $this->getData('datePosted');
}
/**
* set date posted
*
* @param string $datePosted
*/
public function setDatePosted($datePosted)
{
$this->setData('datePosted', $datePosted);
}
/**
* get date modified
*
* @return string
*/
public function getDateModified()
{
return $this->getData('dateModified');
}
/**
* set date modified
*
* @param string $dateModified
*/
public function setDateModified($dateModified)
{
$this->setData('dateModified', $dateModified);
}
/**
* get viewable
*
* @return bool
*/
public function getViewable()
{
return $this->getData('viewable');
}
/**
* set viewable
*
* @param bool $viewable
*/
public function setViewable($viewable)
{
$this->setData('viewable', $viewable);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionComment', '\SubmissionComment');
foreach (['COMMENT_TYPE_PEER_REVIEW', 'COMMENT_TYPE_EDITOR_DECISION', 'COMMENT_TYPE_COPYEDIT', 'COMMENT_TYPE_LAYOUT', 'COMMENT_TYPE_PROOFREAD'] as $constantName) {
define($constantName, constant('\SubmissionComment::' . $constantName));
}
}
@@ -0,0 +1,283 @@
<?php
/**
* @file classes/submission/SubmissionCommentDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionCommentDAO
*
* @ingroup submission
*
* @see SubmissionComment
*
* @brief Operations for retrieving and modifying SubmissionComment objects.
*/
namespace PKP\submission;
use PKP\db\DAOResultFactory;
use PKP\plugins\Hook;
class SubmissionCommentDAO extends \PKP\db\DAO
{
/**
* Retrieve SubmissionComments by submission id
*
* @param int $submissionId Submission ID
* @param int $commentType Comment type
* @param int $assocId Assoc ID
*
* @return DAOResultFactory<SubmissionComment>
*/
public function getSubmissionComments($submissionId, $commentType = null, $assocId = null)
{
$params = [(int) $submissionId];
if ($commentType) {
$params[] = (int) $commentType;
}
if ($assocId) {
$params[] = (int) $assocId;
}
return new DAOResultFactory(
$this->retrieve(
'SELECT a.*
FROM submission_comments a
WHERE submission_id = ?'
. ($commentType ? ' AND comment_type = ?' : '')
. ($assocId ? ' AND assoc_id = ?' : '')
. ' ORDER BY date_posted',
$params
),
$this,
'_fromRow'
);
}
/**
* Retrieve SubmissionComments by user id
*
* @param int $userId User ID.
*
* @return DAOResultFactory<SubmissionComment>
*/
public function getByUserId($userId)
{
return new DAOResultFactory(
$this->retrieve(
'SELECT a.* FROM submission_comments a WHERE author_id = ? ORDER BY date_posted',
[(int) $userId]
),
$this,
'_fromRow'
);
}
/**
* Retrieve SubmissionComments made my reviewers on a submission
*
* @param int $submissionId The submission Id that was reviewered/commented on.
* @param int $reviewerId The user id of the reviewer.
* @param int $reviewId (optional) The review assignment ID the comment pertains to.
* @param bool $viewable True for only viewable comments; false for non-viewable; null for both
*
* @return DAOResultFactory<SubmissionComment>
*/
public function getReviewerCommentsByReviewerId($submissionId, $reviewerId = null, $reviewId = null, $viewable = null)
{
$params = [(int) $submissionId];
if ($reviewerId) {
$params[] = (int) $reviewerId;
}
if ($reviewId) {
$params[] = (int) $reviewId;
}
return new DAOResultFactory(
$this->retrieve(
$sql = 'SELECT a.*
FROM submission_comments a
WHERE submission_id = ?
' . ($reviewerId ? ' AND author_id = ?' : '') . '
' . ($reviewId ? ' AND assoc_id = ?' : '') . '
' . ($viewable === true ? ' AND viewable = 1' : '') . '
' . ($viewable === false ? ' AND viewable = 0' : '') . '
ORDER BY date_posted DESC',
$params
),
$this,
'_fromRow',
[],
$sql,
$params // Counted in readReview.tpl and authorReadReview.tpl
);
}
/**
* Retrieve submission comment by id
*
* @param int $commentId Comment ID.
*
* @return SubmissionComment object
*/
public function getById($commentId)
{
$result = $this->retrieve(
'SELECT * FROM submission_comments WHERE comment_id = ?',
[(int) $commentId]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Construct a new \PKP\core\DataObject.
*
* @return SubmissionComment
*/
public function newDataObject()
{
return new SubmissionComment();
}
/**
* Creates and returns a submission comment object from a row
*
* @param array $row
*
* @return SubmissionComment object
*/
public function _fromRow($row)
{
$submissionComment = $this->newDataObject();
$submissionComment->setId($row['comment_id']);
$submissionComment->setCommentType($row['comment_type']);
$submissionComment->setRoleId($row['role_id']);
$submissionComment->setSubmissionId($row['submission_id']);
$submissionComment->setAssocId($row['assoc_id']);
$submissionComment->setAuthorId($row['author_id']);
$submissionComment->setCommentTitle($row['comment_title']);
$submissionComment->setComments($row['comments']);
$submissionComment->setDatePosted($this->datetimeFromDB($row['date_posted']));
$submissionComment->setDateModified($this->datetimeFromDB($row['date_modified']));
$submissionComment->setViewable($row['viewable']);
Hook::call('SubmissionCommentDAO::_fromRow', [&$submissionComment, &$row]);
return $submissionComment;
}
/**
* inserts a new submission comment into the submission_comments table
*
* @param SubmissionComment $submissionComment object
*
* @return int note ID int
*/
public function insertObject($submissionComment)
{
$this->update(
sprintf(
'INSERT INTO submission_comments
(comment_type, role_id, submission_id, assoc_id, author_id, date_posted, date_modified, comment_title, comments, viewable)
VALUES
(?, ?, ?, ?, ?, %s, %s, ?, ?, ?)',
$this->datetimeToDB($submissionComment->getDatePosted()),
$this->datetimeToDB($submissionComment->getDateModified())
),
[
(int) $submissionComment->getCommentType(),
(int) $submissionComment->getRoleId(),
(int) $submissionComment->getSubmissionId(),
(int) $submissionComment->getAssocId(),
(int) $submissionComment->getAuthorId(),
$submissionComment->getCommentTitle(),
$submissionComment->getComments(),
(int) $submissionComment->getViewable()
]
);
$submissionComment->setId($this->getInsertId());
return $submissionComment->getId();
}
/**
* Removes a submission comment from the submission_comments table
*
* @param SubmissionComment $submissionComment object
*/
public function deleteObject($submissionComment)
{
$this->deleteById($submissionComment->getId());
}
/**
* Removes a submission note by id
*
* @param int $commentId
*/
public function deleteById($commentId)
{
$this->update(
'DELETE FROM submission_comments WHERE comment_id = ?',
[(int) $commentId]
);
}
/**
* Delete all comments for a submission.
*
* @param int $submissionId
*/
public function deleteBySubmissionId($submissionId)
{
$this->update(
'DELETE FROM submission_comments WHERE submission_id = ?',
[(int) $submissionId]
);
}
/**
* Updates a submission comment
*
* @param SubmissionComment $submissionComment object
*/
public function updateObject($submissionComment)
{
$this->update(
sprintf(
'UPDATE submission_comments
SET
comment_type = ?,
role_id = ?,
submission_id = ?,
assoc_id = ?,
author_id = ?,
date_posted = %s,
date_modified = %s,
comment_title = ?,
comments = ?,
viewable = ?
WHERE comment_id = ?',
$this->datetimeToDB($submissionComment->getDatePosted()),
$this->datetimeToDB($submissionComment->getDateModified())
),
[
(int) $submissionComment->getCommentType(),
(int) $submissionComment->getRoleId(),
(int) $submissionComment->getSubmissionId(),
(int) $submissionComment->getAssocId(),
(int) $submissionComment->getAuthorId(),
$submissionComment->getCommentTitle(),
$submissionComment->getComments(),
$submissionComment->getViewable() === null ? 1 : (int) $submissionComment->getViewable(),
(int) $submissionComment->getId()
]
);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionCommentDAO', '\SubmissionCommentDAO');
}
@@ -0,0 +1,56 @@
<?php
/**
* @file classes/submission/SubmissionDiscipline.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionDiscipline
*
* @ingroup submission
*
* @see SubmissionDisciplineEntryDAO
*
* @brief Basic class describing a submission discipline
*/
namespace PKP\submission;
class SubmissionDiscipline extends \PKP\controlledVocab\ControlledVocabEntry
{
//
// Get/set methods
//
/**
* Get the discipline
*
* @return string
*/
public function getDiscipline()
{
return $this->getData('submissionDiscipline');
}
/**
* Set the discipline text
*
* @param string $discipline
* @param string $locale
*/
public function setDiscipline($discipline, $locale)
{
$this->setData('submissionDiscipline', $discipline, $locale);
}
public function getLocaleMetadataFieldNames()
{
return ['submissionDiscipline'];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionDiscipline', '\SubmissionDiscipline');
}
@@ -0,0 +1,142 @@
<?php
/**
* @file classes/submission/SubmissionDisciplineDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionDisciplineDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's assigned
* disciplines
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocab;
use PKP\controlledVocab\ControlledVocabDAO;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
class SubmissionDisciplineDAO extends ControlledVocabDAO
{
public const CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE = 'submissionDiscipline';
/**
* Build/fetch a publication's discipline controlled vocabulary.
*
* @param int $publicationId
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*
* @return ControlledVocab
*/
public function build($publicationId, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
return parent::_build(self::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE, $assocType, $publicationId);
}
/**
* Get the list of localized additional fields to store.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['submissionDiscipline'];
}
/**
* Get disciplines for a submission.
*
* @param int $publicationId
* @param array $locales
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#6213
*
* @return array
*/
public function getDisciplines($publicationId, $locales = [], $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$result = [];
$disciplines = $this->build($publicationId, $assocType);
$submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */
$submissionDisciplines = $submissionDisciplineEntryDao->getByControlledVocabId($disciplines->getId());
while ($disciplineEntry = $submissionDisciplines->next()) {
$discipline = $disciplineEntry->getDiscipline();
foreach ($discipline as $locale => $value) {
if (empty($locales) || in_array($locale, $locales)) {
$result[$locale][] = $value;
}
}
}
return $result;
}
/**
* Get an array of all of the submission's disciplines
*
* @return array
*/
public function getAllUniqueDisciplines()
{
$result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE]);
$disciplines = [];
foreach ($result as $row) {
$disciplines[] = $row->setting_value;
}
return $disciplines;
}
/**
* Add an array of disciplines
*
* @param array $disciplines
* @param int $publicationId
* @param bool $deleteFirst
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*/
public function insertDisciplines($disciplines, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$disciplineDao = DAORegistry::getDAO('SubmissionDisciplineDAO'); /** @var SubmissionDisciplineDAO $disciplineDao */
$submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */
$currentDisciplines = $this->build($publicationId, $assocType);
if ($deleteFirst) {
$existingEntries = $disciplineDao->enumerate($currentDisciplines->getId(), self::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE);
foreach ($existingEntries as $id => $entry) {
$entry = trim($entry);
$submissionDisciplineEntryDao->deleteObjectById($id);
}
}
if (is_array($disciplines)) { // localized, array of arrays
foreach ($disciplines as $locale => $list) {
if (is_array($list)) {
$list = array_unique($list); // Remove any duplicate keywords
$i = 1;
foreach ($list as $discipline) {
$disciplineEntry = $submissionDisciplineEntryDao->newDataObject();
$disciplineEntry->setControlledVocabId($currentDisciplines->getId());
$disciplineEntry->setDiscipline($discipline, $locale);
$disciplineEntry->setSequence($i);
$i++;
$submissionDisciplineEntryDao->insertObject($disciplineEntry);
}
}
}
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionDisciplineDAO', '\SubmissionDisciplineDAO');
define('CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE', SubmissionDisciplineDAO::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE);
}
@@ -0,0 +1,61 @@
<?php
/**
* @file classes/submission/SubmissionDisciplineEntryDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionDisciplineEntryDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's disciplines
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocabEntryDAO;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
class SubmissionDisciplineEntryDAO extends ControlledVocabEntryDAO
{
/**
* Construct a new data object corresponding to this DAO.
*
* @return SubmissionDiscipline
*/
public function newDataObject()
{
return new SubmissionDiscipline();
}
/**
* Retrieve an iterator of controlled vocabulary entries matching a
* particular controlled vocabulary ID.
*
* @param int $controlledVocabId
* @param mixed $filter (Not yet supported)
* @param null|DBResultRange $rangeInfo
*
* @return DAOResultFactory<SubmissionDiscipline> matching CVE objects
*/
public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null)
{
assert($filter == null); // Parent class supports this, but this class does not
$result = $this->retrieveRange(
'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq',
[(int) $controlledVocabId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionDisciplineEntryDAO', '\SubmissionDisciplineEntryDAO');
}
@@ -0,0 +1,56 @@
<?php
/**
* @file classes/submission/SubmissionKeyword.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionKeyword
*
* @ingroup submission
*
* @see SubmissionKeywordEntryDAO
*
* @brief Basic class describing a submission keyword
*/
namespace PKP\submission;
class SubmissionKeyword extends \PKP\controlledVocab\ControlledVocabEntry
{
//
// Get/set methods
//
/**
* Get the keyword
*
* @return string
*/
public function getKeyword()
{
return $this->getData('submissionKeyword');
}
/**
* Set the keyword text
*
* @param string $keyword
* @param string $locale
*/
public function setKeyword($keyword, $locale)
{
$this->setData('submissionKeyword', $keyword, $locale);
}
public function getLocaleMetadataFieldNames()
{
return ['submissionKeyword'];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionKeyword', '\SubmissionKeyword');
}
@@ -0,0 +1,161 @@
<?php
/**
* @file classes/submission/SubmissionKeywordDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionKeywordDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's assigned keywords
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocab;
use PKP\controlledVocab\ControlledVocabDAO;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
class SubmissionKeywordDAO extends ControlledVocabDAO
{
public const CONTROLLED_VOCAB_SUBMISSION_KEYWORD = 'submissionKeyword';
/**
* Build/fetch and return a controlled vocabulary for keywords.
*
* @param int $publicationId
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*
* @return ControlledVocab
*/
public function build($publicationId, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
// may return an array of ControlledVocabs
return parent::_build(self::CONTROLLED_VOCAB_SUBMISSION_KEYWORD, $assocType, $publicationId);
}
/**
* Get the list of localized additional fields to store.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['submissionKeyword'];
}
/**
* Get keywords for a submission.
*
* @param int $publicationId
* @param array $locales
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#6213
*
* @return array
*/
public function getKeywords($publicationId, $locales = [], $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$result = [];
$keywords = $this->build($publicationId, $assocType);
$submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var SubmissionKeywordEntryDAO $submissionKeywordEntryDao */
$submissionKeywords = $submissionKeywordEntryDao->getByControlledVocabId($keywords->getId());
while ($keywordEntry = $submissionKeywords->next()) {
$keyword = $keywordEntry->getKeyword();
foreach ($keyword as $locale => $value) {
if (empty($locales) || in_array($locale, $locales)) {
if (!array_key_exists($locale, $result)) {
$result[$locale] = [];
}
$result[$locale][] = $value;
}
}
}
return $result;
}
/**
* Get an array of all of the submission's keywords
*
* @return array
*/
public function getAllUniqueKeywords()
{
$result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_KEYWORD]);
$keywords = [];
foreach ($result as $row) {
$keywords[] = $row->setting_value;
}
return $keywords;
}
/**
* Add an array of keywords
*
* @param array $keywords
* @param int $publicationId
* @param bool $deleteFirst
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*/
public function insertKeywords($keywords, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var SubmissionKeywordEntryDAO $submissionKeywordEntryDao */
if ($deleteFirst) {
$currentKeywords = $this->deleteByPublicationId($publicationId);
} else {
$currentKeywords = $this->build($publicationId, $assocType);
}
if (is_array($keywords)) { // localized, array of arrays
foreach ($keywords as $locale => $list) {
if (is_array($list)) {
$list = array_unique($list); // Remove any duplicate keywords
$i = 1;
foreach ($list as $keyword) {
$keywordEntry = $submissionKeywordEntryDao->newDataObject();
$keywordEntry->setControlledVocabId($currentKeywords->getId());
$keywordEntry->setKeyword($keyword, $locale);
$keywordEntry->setSequence($i);
$i++;
$submissionKeywordEntryDao->insertObject($keywordEntry);
}
}
}
}
}
/**
* Delete keywords by publication ID
*
* @return ControlledVocab Controlled Vocab
*/
public function deleteByPublicationId($publicationId)
{
$keywordDao = DAORegistry::getDAO('SubmissionKeywordDAO'); /** @var SubmissionKeywordDAO $keywordDao */
$submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var SubmissionKeywordEntryDAO $submissionKeywordEntryDao */
$currentKeywords = $this->build($publicationId);
$existingEntries = $keywordDao->enumerate($currentKeywords->getId(), self::CONTROLLED_VOCAB_SUBMISSION_KEYWORD);
foreach ($existingEntries as $id => $entry) {
$entry = trim($entry);
$entryObj = $submissionKeywordEntryDao->getById($id);
$submissionKeywordEntryDao->deleteObjectById($id);
}
return $currentKeywords;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionKeywordDAO', '\SubmissionKeywordDAO');
define('CONTROLLED_VOCAB_SUBMISSION_KEYWORD', SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD);
}
@@ -0,0 +1,62 @@
<?php
/**
* @file classes/submission/SubmissionKeywordEntryDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionKeywordEntryDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's keywords
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocabEntryDAO;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
class SubmissionKeywordEntryDAO extends ControlledVocabEntryDAO
{
/**
* Construct a new data object corresponding to this DAO.
*
* @return SubmissionKeyword
*/
public function newDataObject()
{
return new SubmissionKeyword();
}
/**
* Retrieve an iterator of controlled vocabulary entries matching a
* particular controlled vocabulary ID.
*
* @param int $controlledVocabId
* @param mixed $filter (Not yet supported)
* @param ?DBResultRange $rangeInfo
*
* @return DAOResultFactory<SubmissionKeyword> Object containing matching CVE objects
*/
public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null)
{
assert($filter == null); // Parent class supports this, but this class does not
$result = $this->retrieveRange(
'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq',
[(int) $controlledVocabId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionKeywordEntryDAO', '\SubmissionKeywordEntryDAO');
}
@@ -0,0 +1,59 @@
<?php
/**
* @file classes/submission/SubmissionLanguage.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionLanguage
*
* @ingroup submission
*
* @see SubmissionLanguageEntryDAO
*
* @brief Basic class describing a submission language
*/
namespace PKP\submission;
class SubmissionLanguage extends \PKP\controlledVocab\ControlledVocabEntry
{
//
// Get/set methods
//
/**
* Get the language
*
* @return string
*/
public function getLanguage()
{
return $this->getData('submissionLanguage');
}
/**
* Set the language text
*
* @param string $language
* @param string $locale
*/
public function setLanguage($language, $locale)
{
$this->setData('submissionLanguage', $language, $locale);
}
/**
* @copydoc \PKP\controlledVocab\ControlledVocabEntry::getLocaleMetadataFieldNames()
*/
public function getLocaleMetadataFieldNames()
{
return ['submissionLanguage'];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionLanguage', '\SubmissionLanguage');
}
@@ -0,0 +1,142 @@
<?php
/**
* @file classes/submission/SubmissionLanguageDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionLanguageDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's assigned languages
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocab;
use PKP\controlledVocab\ControlledVocabDAO;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
class SubmissionLanguageDAO extends ControlledVocabDAO
{
public const CONTROLLED_VOCAB_SUBMISSION_LANGUAGE = 'submissionLanguage';
/**
* Build/fetch and return a controlled vocabulary for languages.
*
* @param int $publicationId
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*
* @return ControlledVocab
*/
public function build($publicationId, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
// may return an array of ControlledVocabs
return parent::_build(self::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE, $assocType, $publicationId);
}
/**
* Get the list of localized additional fields to store.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['submissionLanguage'];
}
/**
* Get Languages for a submission.
*
* @param int $publicationId
* @param array $locales
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#6213
*
* @return array
*/
public function getLanguages($publicationId, $locales = [], $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$result = [];
$languages = $this->build($publicationId, $assocType);
$submissionLanguageEntryDao = DAORegistry::getDAO('SubmissionLanguageEntryDAO'); /** @var SubmissionLanguageEntryDAO $submissionLanguageEntryDao */
$submissionLanguages = $submissionLanguageEntryDao->getByControlledVocabId($languages->getId());
while ($languageEntry = $submissionLanguages->next()) {
$language = $languageEntry->getLanguage();
foreach ($language as $locale => $value) {
if (empty($locales) || in_array($locale, $locales)) {
$result[$locale][] = $value;
}
}
}
return $result;
}
/**
* Get an array of all of the submission's Languages
*
* @return array
*/
public function getAllUniqueLanguages()
{
$result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [self::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE]);
$languages = [];
foreach ($result as $row) {
$languages[] = $row->setting_value;
}
return $languages;
}
/**
* Add an array of languages
*
* @param array $languages
* @param int $publicationId
* @param bool $deleteFirst
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*/
public function insertLanguages($languages, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$languageDao = DAORegistry::getDAO('SubmissionLanguageDAO'); /** @var SubmissionLanguageDAO $languageDao */
$submissionLanguageEntryDao = DAORegistry::getDAO('SubmissionLanguageEntryDAO'); /** @var SubmissionLanguageEntryDAO $submissionLanguageEntryDao */
$currentLanguages = $this->build($publicationId, $assocType);
if ($deleteFirst) {
$existingEntries = $languageDao->enumerate($currentLanguages->getId(), self::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE);
foreach ($existingEntries as $id => $entry) {
$entry = trim($entry);
$submissionLanguageEntryDao->deleteObjectById($id);
}
}
if (is_array($languages)) { // localized, array of arrays
foreach ($languages as $locale => $list) {
if (is_array($list)) {
$list = array_unique($list); // Remove any duplicate Languages
$i = 1;
foreach ($list as $language) {
$languageEntry = $submissionLanguageEntryDao->newDataObject();
$languageEntry->setControlledVocabId($currentLanguages->getId());
$languageEntry->setLanguage($language, $locale);
$languageEntry->setSequence($i);
$i++;
$submissionLanguageEntryDao->insertObject($languageEntry);
}
}
}
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionLanguageDAO', '\SubmissionLanguageDAO');
define('CONTROLLED_VOCAB_SUBMISSION_LANGUAGE', SubmissionLanguageDAO::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE);
}
@@ -0,0 +1,62 @@
<?php
/**
* @file classes/submission/SubmissionLanguageEntryDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionLanguageEntryDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's languages
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocabEntryDAO;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
class SubmissionLanguageEntryDAO extends ControlledVocabEntryDAO
{
/**
* Construct a new data object corresponding to this DAO.
*
* @return submissionLanguage
*/
public function newDataObject()
{
return new SubmissionLanguage();
}
/**
* Retrieve an iterator of controlled vocabulary entries matching a
* particular controlled vocabulary ID.
*
* @param int $controlledVocabId
* @param mixed $filter (Not yet supported)
* @param ?DBResultRange $rangeInfo
*
* @return DAOResultFactory<SubmissionLanguage> Object containing matching CVE objects
*/
public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null)
{
assert($filter == null); // Parent class supports this, but this class does not
$result = $this->retrieveRange(
'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq',
[(int) $controlledVocabId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionLanguageEntryDAO', '\SubmissionLanguageEntryDAO');
}
@@ -0,0 +1,59 @@
<?php
/**
* @file classes/submission/SubmissionSubject.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionSubject
*
* @ingroup submission
*
* @see SubmissionSubjectEntryDAO
*
* @brief Basic class describing a submission subject
*/
namespace PKP\submission;
class SubmissionSubject extends \PKP\controlledVocab\ControlledVocabEntry
{
//
// Get/set methods
//
/**
* Get the subject
*
* @return array
*/
public function getSubject()
{
return $this->getData('submissionSubject');
}
/**
* Set the subject text
*
* @param string $subject
* @param string $locale
*/
public function setSubject($subject, $locale)
{
$this->setData('submissionSubject', $subject, $locale);
}
/**
* @copydoc \PKP\controlledVocab\ControlledVocabEntry::getLocaleMetadataFieldNames()
*/
public function getLocaleMetadataFieldNames()
{
return ['submissionSubject'];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionSubject', '\SubmissionSubject');
}
@@ -0,0 +1,143 @@
<?php
/**
* @file classes/submission/SubmissionSubjectDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionSubjectDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's assigned subjects
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocab;
use PKP\controlledVocab\ControlledVocabDAO;
use PKP\core\PKPApplication;
use PKP\db\DAORegistry;
class SubmissionSubjectDAO extends ControlledVocabDAO
{
public const CONTROLLED_VOCAB_SUBMISSION_SUBJECT = 'submissionSubject';
/**
* Build/fetch and return a controlled vocabulary for subjects.
*
* @param int $publicationId
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*
* @return ControlledVocab
*/
public function build($publicationId, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
// may return an array of ControlledVocabs
return parent::_build(SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT, $assocType, $publicationId);
}
/**
* Get the list of localized additional fields to store.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['submissionSubject'];
}
/**
* Get Subjects for a submission.
*
* @param int $publicationId
* @param array $locales
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#6213
*
* @return array
*/
public function getSubjects($publicationId, $locales = [], $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$result = [];
$subjects = $this->build($publicationId, $assocType);
$submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var SubmissionSubjectEntryDAO $submissionSubjectEntryDao */
$submissionSubjects = $submissionSubjectEntryDao->getByControlledVocabId($subjects->getId());
/** @var SubmissionSubject */
foreach ($submissionSubjects->toIterator() as $subjectEntry) {
$subject = $subjectEntry->getSubject();
foreach ($subject as $locale => $value) {
if (empty($locales) || in_array($locale, $locales)) {
$result[$locale][] = $value;
}
}
}
return $result;
}
/**
* Get an array of all of the submission's Subjects
*
* @return array
*/
public function getAllUniqueSubjects()
{
$result = $this->retrieve('SELECT DISTINCT setting_value FROM controlled_vocab_entry_settings WHERE setting_name = ?', [SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT]);
$subjects = [];
foreach ($result as $row) {
$subjects[] = $row->setting_value;
}
return $subjects;
}
/**
* Add an array of subjects
*
* @param array $subjects
* @param int $publicationId
* @param bool $deleteFirst
* @param int $assocType DO NOT USE: For <3.1 to 3.x migration pkp/pkp-lib#3572 pkp/pkp-lib#6213
*/
public function insertSubjects($subjects, $publicationId, $deleteFirst = true, $assocType = PKPApplication::ASSOC_TYPE_PUBLICATION)
{
$subjectDao = DAORegistry::getDAO('SubmissionSubjectDAO'); /** @var SubmissionSubjectDAO $subjectDao */
$submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var SubmissionSubjectEntryDAO $submissionSubjectEntryDao */
$currentSubjects = $this->build($publicationId, $assocType);
if ($deleteFirst) {
$existingEntries = $subjectDao->enumerate($currentSubjects->getId(), SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT);
foreach ($existingEntries as $id => $entry) {
$entry = trim($entry);
$submissionSubjectEntryDao->deleteObjectById($id);
}
}
if (is_array($subjects)) { // localized, array of arrays
foreach ($subjects as $locale => $list) {
if (is_array($list)) {
$list = array_unique($list); // Remove any duplicate Subjects
$i = 1;
foreach ($list as $subject) {
$subjectEntry = $submissionSubjectEntryDao->newDataObject();
$subjectEntry->setControlledVocabId($currentSubjects->getId());
$subjectEntry->setSubject($subject, $locale);
$subjectEntry->setSequence($i);
$i++;
$submissionSubjectEntryDao->insertObject($subjectEntry);
}
}
}
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionSubjectDAO', '\SubmissionSubjectDAO');
define('CONTROLLED_VOCAB_SUBMISSION_SUBJECT', SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT);
}
@@ -0,0 +1,61 @@
<?php
/**
* @file classes/submission/SubmissionSubjectEntryDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SubmissionSubjectEntryDAO
*
* @ingroup submission
*
* @see Submission
*
* @brief Operations for retrieving and modifying a submission's subjects
*/
namespace PKP\submission;
use PKP\controlledVocab\ControlledVocabEntryDAO;
use PKP\db\DAOResultFactory;
use PKP\db\DBResultRange;
class SubmissionSubjectEntryDAO extends ControlledVocabEntryDAO
{
/**
* Construct a new data object corresponding to this DAO.
*
* @return submissionSubject
*/
public function newDataObject()
{
return new SubmissionSubject();
}
/**
* Retrieve an iterator of controlled vocabulary entries matching a
* particular controlled vocabulary ID.
*
* @param int $controlledVocabId
* @param mixed $filter (Not yet supported)
* @param ?DBResultRange $rangeInfo
*
* @return DAOResultFactory<SubmissionSubject> Object containing matching CVE objects
*/
public function getByControlledVocabId($controlledVocabId, $rangeInfo = null, $filter = null)
{
assert($filter == null); // Parent class supports this, but this class does not
$result = $this->retrieveRange(
'SELECT cve.* FROM controlled_vocab_entries cve WHERE cve.controlled_vocab_id = ? ORDER BY seq',
[(int) $controlledVocabId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\SubmissionSubjectEntryDAO', '\SubmissionSubjectEntryDAO');
}
@@ -0,0 +1,272 @@
<?php
/**
* @file classes/submission/action/EditorAction.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class EditorAction
*
* @ingroup submission_action
*
* @brief Editor actions.
*/
namespace PKP\submission\action;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Illuminate\Support\Facades\Mail;
use PKP\context\Context;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\core\PKPServices;
use PKP\core\PKPString;
use PKP\db\DAORegistry;
use PKP\log\event\PKPSubmissionEventLogEntry;
use PKP\mail\mailables\ReviewRequest;
use PKP\mail\mailables\ReviewRequestSubsequent;
use PKP\mail\variables\ReviewAssignmentEmailVariable;
use PKP\notification\PKPNotification;
use PKP\notification\PKPNotificationManager;
use PKP\plugins\Hook;
use PKP\security\AccessKeyManager;
use PKP\security\Validation;
use PKP\submission\PKPSubmission;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\user\User;
use Symfony\Component\Mailer\Exception\TransportException;
class EditorAction
{
/**
* Constructor.
*/
public function __construct()
{
}
//
// Actions.
//
/**
* Assigns a reviewer to a submission.
*
* @param PKPRequest $request
* @param object $submission
* @param int $reviewerId
* @param ReviewRound $reviewRound
* @param string $reviewDueDate
* @param string $responseDueDate
* @param null|mixed $reviewMethod
*/
public function addReviewer($request, $submission, $reviewerId, &$reviewRound, $reviewDueDate, $responseDueDate, $reviewMethod = null)
{
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewer = Repo::user()->get($reviewerId);
// Check to see if the requested reviewer is not already
// assigned to review this submission.
$assigned = $reviewAssignmentDao->reviewerExists($reviewRound->getId(), $reviewerId);
// Only add the reviewer if he has not already
// been assigned to review this submission.
$stageId = $reviewRound->getStageId();
$round = $reviewRound->getRound();
if (!$assigned && isset($reviewer) && !Hook::call('EditorAction::addReviewer', [&$submission, $reviewerId])) {
$reviewAssignment = $reviewAssignmentDao->newDataObject();
$reviewAssignment->setSubmissionId($submission->getId());
$reviewAssignment->setReviewerId($reviewerId);
$reviewAssignment->setDateAssigned(Core::getCurrentDate());
$reviewAssignment->setStageId($stageId);
$reviewAssignment->setRound($round);
$reviewAssignment->setReviewRoundId($reviewRound->getId());
if (isset($reviewMethod)) {
$reviewAssignment->setReviewMethod($reviewMethod);
}
$reviewAssignmentDao->insertObject($reviewAssignment);
$this->setDueDates($request, $submission, $reviewAssignment, $reviewDueDate, $responseDueDate);
// Add notification
$notificationMgr = new NotificationManager();
$notificationMgr->createNotification(
$request,
$reviewerId,
PKPNotification::NOTIFICATION_TYPE_REVIEW_ASSIGNMENT,
$submission->getContextId(),
PKPApplication::ASSOC_TYPE_REVIEW_ASSIGNMENT,
$reviewAssignment->getId(),
Notification::NOTIFICATION_LEVEL_TASK
);
// Add log
$user = $request->getUser();
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => PKPSubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_ASSIGN,
'userId' => Validation::loggedInAs() ?? $user->getId(),
'message' => 'log.review.reviewerAssigned',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
'reviewAssignment' => $reviewAssignment->getId(),
'reviewerName' => $reviewer->getFullName(),
'submissionId' => $submission->getId(),
'stageId' => $stageId,
'round' => $round
]);
Repo::eventLog()->add($eventLog);
// Send mail
if (!$request->getUserVar('skipEmail')) {
$context = PKPServices::get('context')->get($submission->getData('contextId'));
$emailTemplate = Repo::emailTemplate()->getByKey($submission->getData('contextId'), $request->getUserVar('template'));
$emailBody = $request->getUserVar('personalMessage');
$emailSubject = $emailTemplate->getLocalizedData('subject');
$mailable = $this->createMail($submission, $reviewAssignment, $reviewer, $user, $emailBody, $emailSubject, $context);
try {
Mail::send($mailable);
} catch (TransportException $e) {
$notificationMgr = new PKPNotificationManager();
$notificationMgr->createTrivialNotification(
$user->getId(),
PKPNotification::NOTIFICATION_TYPE_ERROR,
['contents' => __('email.compose.error')]
);
trigger_error('Failed to send email: ' . $e->getMessage(), E_USER_WARNING);
}
}
}
}
/**
* Sets the due date for a review assignment.
*
* @param PKPRequest $request
* @param Submission $submission
* @param ReviewAssignment $reviewAssignment
* @param string $reviewDueDate
* @param string $responseDueDate
* @param bool $logEntry
*/
public function setDueDates($request, $submission, $reviewAssignment, $reviewDueDate, $responseDueDate, $logEntry = false)
{
$context = $request->getContext();
$reviewer = Repo::user()->get($reviewAssignment->getReviewerId());
if (!isset($reviewer)) {
return false;
}
if ($reviewAssignment->getSubmissionId() == $submission->getId() && !Hook::call('EditorAction::setDueDates', [&$reviewAssignment, &$reviewer, &$reviewDueDate, &$responseDueDate])) {
// Set the review due date
$defaultNumWeeks = $context->getData('numWeeksPerReview');
$reviewAssignment->setDateDue($reviewDueDate);
// Set the response due date
$defaultNumWeeks = $context->getData('numWeeksPerResponse');
$reviewAssignment->setDateResponseDue($responseDueDate);
// update the assignment (with both the new dates)
$reviewAssignment->stampModified();
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignmentDao->updateObject($reviewAssignment);
// N.B. Only logging Date Due
if ($logEntry) {
// Add log
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => PKPSubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_SET_DUE_DATE,
'userId' => Validation::loggedInAs() ?? $request->getUser()->getId(),
'message' => 'log.review.reviewDueDateSet',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
'reviewAssignmentId' => $reviewAssignment->getId(),
'reviewerName' => $reviewer->getFullName(),
'reviewDueDate' => date(
PKPString::convertStrftimeFormat($context->getLocalizedDateFormatShort()),
strtotime($reviewAssignment->getDateDue())
),
'submissionId' => $submission->getId(),
'stageId' => $reviewAssignment->getStageId(),
'round' => $reviewAssignment->getRound()
]);
Repo::eventLog()->add($eventLog);
}
}
}
/**
* Create an email representation based on data entered by the editor to the ReviewerForm
* Associated templates: REVIEW_REQUEST, REVIEW_REQUEST_SUBSEQUENT
*/
protected function createMail(
PKPSubmission $submission,
ReviewAssignment $reviewAssignment,
User $reviewer,
User $sender,
string $emailBody,
string $emailSubject,
Context $context
): ReviewRequest|ReviewRequestSubsequent {
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getById($reviewAssignment->getReviewRoundId());
$mailable = $reviewRound->getRound() == 1 ?
new ReviewRequest($context, $submission, $reviewAssignment) :
new ReviewRequestSubsequent($context, $submission, $reviewAssignment);
if ($context->getData('reviewerAccessKeysEnabled')) {
$accessKeyManager = new AccessKeyManager();
$expiryDays = ($context->getData('numWeeksPerReview') + 4) * 7;
$accessKey = $accessKeyManager->createKey($context->getId(), $reviewer->getId(), $reviewAssignment->getId(), $expiryDays);
$mailable->buildViewDataUsing(function () use ($context, $reviewAssignment, $accessKey) {
return [
ReviewAssignmentEmailVariable::REVIEW_ASSIGNMENT_URL => PKPApplication::get()->getDispatcher()->url(
PKPApplication::get()->getRequest(),
PKPApplication::ROUTE_PAGE,
$context->getData('urlPath'),
'reviewer',
'submission',
null,
[
'submissionId' => $reviewAssignment->getSubmissionId(),
'reviewId' => $reviewAssignment->getId(),
'key' => $accessKey,
]
)
];
});
}
$mailable
->body($emailBody)
->subject($emailSubject)
->sender($sender)
->recipients([$reviewer]);
// Additional template variable
$mailable->addData([
'reviewerName' => $mailable->viewData['userFullName'] ?? null,
'reviewerUserName' => $mailable->viewData['username'] ?? null,
]);
return $mailable;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\action\EditorAction', '\EditorAction');
}
+481
View File
@@ -0,0 +1,481 @@
<?php
/**
* @file classes/submission/maps/Schema.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Schema
*
* @brief Map submissions to the properties defined in the submission schema
*/
namespace PKP\submission\maps;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use PKP\db\DAORegistry;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\query\QueryDAO;
use PKP\services\PKPSchemaService;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\Genre;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
use PKP\userGroup\UserGroup;
use PKP\workflow\WorkflowStageDAO;
class Schema extends \PKP\core\maps\Schema
{
/** @copydoc \PKP\core\maps\Schema::$collection */
public Enumerable $collection;
/** @copydoc \PKP\core\maps\Schema::$schema */
public string $schema = PKPSchemaService::SCHEMA_SUBMISSION;
/** @var LazyCollection<int,UserGroup> The user groups for this context. */
public LazyCollection $userGroups;
/** @var Genre[] The file genres in this context. */
public array $genres;
/**
* Get extra property names used in the submissions list
*/
protected function getSubmissionsListProps(): array
{
PluginRegistry::loadCategory('pubIds', true);
$props = [
'_href',
'contextId',
'currentPublicationId',
'dateLastActivity',
'dateSubmitted',
'id',
'lastModified',
'publications',
'reviewAssignments',
'reviewRounds',
'stageId',
'stages',
'status',
'statusLabel',
'submissionProgress',
'urlAuthorWorkflow',
'urlEditorialWorkflow',
'urlWorkflow',
'urlPublished',
];
Hook::call('Submission::getSubmissionsListProps', [&$props]);
return $props;
}
/**
* Map a submission
*
* Includes all properties in the submission schema.
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
* @param Genre[] $genres The file genres in this context
*/
public function map(Submission $item, LazyCollection $userGroups, array $genres): array
{
$this->userGroups = $userGroups;
$this->genres = $genres;
return $this->mapByProperties($this->getProps(), $item);
}
/**
* Summarize a submission
*
* Includes properties with the apiSummary flag in the submission schema.
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
* @param Genre[] $genres The file genres in this context
*/
public function summarize(Submission $item, LazyCollection $userGroups, array $genres): array
{
$this->userGroups = $userGroups;
$this->genres = $genres;
return $this->mapByProperties($this->getSummaryProps(), $item);
}
/**
* Map a collection of Submissions
*
* @see self::map
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
* @param Genre[] $genres The file genres in this context
*/
public function mapMany(Enumerable $collection, LazyCollection $userGroups, array $genres): Enumerable
{
$this->collection = $collection;
$this->userGroups = $userGroups;
$this->genres = $genres;
return $collection->map(function ($item) {
return $this->map($item, $this->userGroups, $this->genres);
});
}
/**
* Summarize a collection of Submissions
*
* @see self::summarize
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
* @param Genre[] $genres The file genres in this context
*/
public function summarizeMany(Enumerable $collection, LazyCollection $userGroups, array $genres): Enumerable
{
$this->collection = $collection;
$this->userGroups = $userGroups;
$this->genres = $genres;
return $collection->map(function ($item) {
return $this->summarize($item, $this->userGroups, $this->genres);
});
}
/**
* Map a submission with extra properties for the submissions list
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
* @param Genre[] $genres The file genres in this context
*/
public function mapToSubmissionsList(Submission $item, LazyCollection $userGroups, array $genres): array
{
$this->userGroups = $userGroups;
$this->genres = $genres;
return $this->mapByProperties($this->getSubmissionsListProps(), $item);
}
/**
* Map a collection of submissions with extra properties for the submissions list
*
* @see self::map
*
* @param LazyCollection<int,UserGroup> $userGroups The user groups in this context
* @param Genre[] $genres The file genres in this context
*/
public function mapManyToSubmissionsList(Enumerable $collection, LazyCollection $userGroups, array $genres): Enumerable
{
$this->collection = $collection;
$this->userGroups = $userGroups;
$this->genres = $genres;
return $collection->map(function ($item) {
return $this->mapToSubmissionsList($item, $this->userGroups, $this->genres);
});
}
/**
* Map a submission with only the title, authors, and URLs for the stats list
*/
public function mapToStats(Submission $submission): array
{
$props = $this->mapByProperties([
'_href',
'id',
'urlWorkflow',
'urlPublished',
], $submission);
$currentPublication = $submission->getCurrentPublication();
if ($currentPublication) {
$props['authorsStringShort'] = $currentPublication->getShortAuthorString();
$props['fullTitle'] = $currentPublication->getFullTitles('html');
}
return $props;
}
/**
* Summarize a submission without publication details
*/
public function summarizeWithoutPublication(Submission $item): array
{
$props = array_filter($this->getSummaryProps(), function ($prop) {
return $prop !== 'publications';
});
return $this->mapByProperties($props, $item);
}
/**
* Map schema properties of a Submission to an assoc array
*/
protected function mapByProperties(array $props, Submission $submission): array
{
$output = [];
if (in_array('publications', $props)) {
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$currentUserReviewAssignment = $reviewAssignmentDao->getLastReviewRoundReviewAssignmentByReviewer(
$submission->getId(),
$this->request->getUser()->getId()
);
$anonymize = $currentUserReviewAssignment && $currentUserReviewAssignment->getReviewMethod() === ReviewAssignment::SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS;
}
foreach ($props as $prop) {
switch ($prop) {
case '_href':
$output[$prop] = Repo::submission()->getUrlApi($this->context, $submission->getId());
break;
case 'publications':
$output[$prop] = Repo::publication()->getSchemaMap($submission, $this->userGroups, $this->genres)
->summarizeMany($submission->getData('publications'), $anonymize)->values();
break;
case 'reviewAssignments':
$output[$prop] = $this->getPropertyReviewAssignments($submission);
break;
case 'reviewRounds':
$output[$prop] = $this->getPropertyReviewRounds($submission);
break;
case 'stages':
$output[$prop] = $this->getPropertyStages($submission);
break;
case 'statusLabel':
$output[$prop] = __($submission->getStatusKey());
break;
case 'urlAuthorWorkflow':
$output[$prop] = Repo::submission()->getUrlAuthorWorkflow($this->context, $submission->getId());
break;
case 'urlEditorialWorkflow':
$output[$prop] = Repo::submission()->getUrlEditorialWorkflow($this->context, $submission->getId());
break;
case 'urlSubmissionWizard':
$output[$prop] = Repo::submission()->getUrlSubmissionWizard($this->context, $submission->getId());
break;
case 'urlWorkflow':
$output[$prop] = Repo::submission()->getWorkflowUrlByUserRoles($submission);
break;
default:
$output[$prop] = $submission->getData($prop);
break;
}
}
return $output;
}
/**
* Get details about the review assignments for a submission
*/
protected function getPropertyReviewAssignments(Submission $submission): array
{
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignments = $reviewAssignmentDao->getBySubmissionId($submission->getId());
$reviews = [];
foreach ($reviewAssignments as $reviewAssignment) {
// @todo for now, only show reviews that haven't been
// declined or cancelled
if ($reviewAssignment->getDeclined() || $reviewAssignment->getCancelled()) {
continue;
}
$request = Application::get()->getRequest();
$currentUser = $request->getUser();
$context = $request->getContext();
$due = is_null($reviewAssignment->getDateDue()) ? null : date('Y-m-d', strtotime($reviewAssignment->getDateDue()));
$responseDue = is_null($reviewAssignment->getDateResponseDue()) ? null : date('Y-m-d', strtotime($reviewAssignment->getDateResponseDue()));
$reviews[] = [
'id' => (int) $reviewAssignment->getId(),
'isCurrentUserAssigned' => $currentUser->getId() == (int) $reviewAssignment->getReviewerId(),
'statusId' => (int) $reviewAssignment->getStatus(),
'status' => __($reviewAssignment->getStatusKey()),
'due' => $due,
'responseDue' => $responseDue,
'round' => (int) $reviewAssignment->getRound(),
'roundId' => (int) $reviewAssignment->getReviewRoundId(),
];
}
return $reviews;
}
/**
* Get details about the review rounds for a submission
*/
protected function getPropertyReviewRounds(Submission $submission): array
{
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRounds = $reviewRoundDao->getBySubmissionId($submission->getId())->toIterator();
$rounds = [];
foreach ($reviewRounds as $reviewRound) {
$rounds[] = [
'id' => $reviewRound->getId(),
'round' => $reviewRound->getRound(),
'stageId' => $reviewRound->getStageId(),
'statusId' => $reviewRound->determineStatus(),
'status' => __($reviewRound->getStatusKey()),
];
}
return $rounds;
}
/**
* Get details about a submission's stage(s)
*
* @return array
* [
* {
* `id` int stage id
* `label` string translated stage name
* `queries` array [{
* `id` int query id
* `assocType` int
* `assocId` int
* `stageId` int
* `seq` int
* `closed` bool
* }]
* `statusId` int stage status. note: on review stage, this refers to the
* status of the latest round.
* `status` string translated stage status name
* `files` array {
* `count` int number of files attached to stage. note: this only counts
* revision files.
* }
* ]
*/
public function getPropertyStages(Submission $submission): array
{
$stageIds = Application::get()->getApplicationStages();
$request = Application::get()->getRequest();
$currentUser = $request->getUser();
$context = $request->getContext();
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignments = $stageAssignmentDao->getBySubmissionAndUserIdAndStageId($submission->getId(), $currentUser->getId() ?? 0)->toArray();
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$openPerStage = $queryDao->countOpenPerStage($submission->getId(), [$request->getUser()->getId()]);
$stages = [];
foreach ($stageIds as $stageId) {
$workflowStageDao = DAORegistry::getDAO('WorkflowStageDAO'); /** @var WorkflowStageDAO $workflowStageDao */
$stage = [
'id' => (int) $stageId,
'label' => __($workflowStageDao->getTranslationKeyFromId($stageId)),
'isActiveStage' => $submission->getData('stageId') == $stageId,
'openQueryCount' => $openPerStage[$stageId],
];
$currentUserAssignedRoles = [];
if ($currentUser) {
/** @var StageAssignment $stageAssignment */
foreach ($stageAssignments as $stageAssignment) {
$userGroup = $this->getUserGroup($stageAssignment->getUserGroupId());
if ($userGroup) {
$currentUserAssignedRoles[] = $userGroup->getRoleId();
}
}
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignmentsResult = $stageAssignmentDao->getBySubmissionAndUserIdAndStageId($submission->getId(), $currentUser->getId(), $stageId);
while ($stageAssignment = $stageAssignmentsResult->next()) {
$userGroup = Repo::userGroup()->get($stageAssignment->getUserGroupId());
$currentUserAssignedRoles[] = (int) $userGroup->getRoleId();
}
}
$stage['currentUserAssignedRoles'] = array_values(array_unique($currentUserAssignedRoles));
// Stage-specific statuses
switch ($stageId) {
case WORKFLOW_STAGE_ID_SUBMISSION:
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$assignedEditors = $stageAssignmentDao->editorAssignedToStage($submission->getId(), $stageId);
if (!$assignedEditors) {
$stage['statusId'] = Repo::submission()::STAGE_STATUS_SUBMISSION_UNASSIGNED;
$stage['status'] = __('submissions.queuedUnassigned');
}
// Submission stage never has revisions
$stage['files'] = [
'count' => 0,
];
break;
case WORKFLOW_STAGE_ID_INTERNAL_REVIEW:
case WORKFLOW_STAGE_ID_EXTERNAL_REVIEW:
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $stageId);
if ($reviewRound) {
$stage['statusId'] = $reviewRound->determineStatus();
$stage['status'] = __($reviewRound->getStatusKey());
// Revision files in this round.
$stage['files'] = [
'count' => Repo::submissionFile()->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages([SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION])
->filterByReviewRoundIds([$reviewRound->getId()])
->getCount()
];
// See if the current user can only recommend:
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$user = $request->getUser();
$editorsStageAssignments = $stageAssignmentDao->getEditorsAssignedToStage($submission->getId(), $stageId);
// if the user is assigned several times in the editorial role, and
// one of the assignments have recommendOnly option set, consider it here
$stage['currentUserCanRecommendOnly'] = false;
foreach ($editorsStageAssignments as $editorsStageAssignment) {
if ($editorsStageAssignment->getUserId() == $user->getId() && $editorsStageAssignment->getRecommendOnly()) {
$stage['currentUserCanRecommendOnly'] = true;
break;
}
}
} else {
// workaround for pkp/pkp-lib#4231, pending formal data model
$stage['files'] = [
'count' => 0
];
}
break;
// Get revision files for editing and production stages.
// Review rounds are handled separately in the review stage below.
case WORKFLOW_STAGE_ID_EDITING:
case WORKFLOW_STAGE_ID_PRODUCTION:
$fileStages = [WORKFLOW_STAGE_ID_EDITING ? SubmissionFile::SUBMISSION_FILE_COPYEDIT : SubmissionFile::SUBMISSION_FILE_PROOF];
// Revision files in this round.
$stage['files'] = [
'count' => Repo::submissionFile()->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages($fileStages)
->getCount()
];
break;
}
$stages[] = $stage;
}
return $stages;
}
protected function getUserGroup(int $userGroupId): ?UserGroup
{
/** @var UserGroup $userGroup */
foreach ($this->userGroups as $userGroup) {
if ($userGroup->getId() === $userGroupId) {
return $userGroup;
}
}
return null;
}
}
@@ -0,0 +1,884 @@
<?php
/**
* @file classes/submission/reviewAssignment/ReviewAssignment.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewAssignment
*
* @ingroup submission
*
* @see ReviewAssignmentDAO
*
* @brief Describes review assignment properties.
*/
namespace PKP\submission\reviewAssignment;
use PKP\core\Core;
class ReviewAssignment extends \PKP\core\DataObject
{
public const SUBMISSION_REVIEWER_RECOMMENDATION_ACCEPT = 1;
public const SUBMISSION_REVIEWER_RECOMMENDATION_PENDING_REVISIONS = 2;
public const SUBMISSION_REVIEWER_RECOMMENDATION_RESUBMIT_HERE = 3;
public const SUBMISSION_REVIEWER_RECOMMENDATION_RESUBMIT_ELSEWHERE = 4;
public const SUBMISSION_REVIEWER_RECOMMENDATION_DECLINE = 5;
public const SUBMISSION_REVIEWER_RECOMMENDATION_SEE_COMMENTS = 6;
public const SUBMISSION_REVIEWER_RATING_VERY_GOOD = 5;
public const SUBMISSION_REVIEWER_RATING_GOOD = 4;
public const SUBMISSION_REVIEWER_RATING_AVERAGE = 3;
public const SUBMISSION_REVIEWER_RATING_POOR = 2;
public const SUBMISSION_REVIEWER_RATING_VERY_POOR = 1;
public const SUBMISSION_REVIEW_METHOD_ANONYMOUS = 1;
public const SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS = 2;
public const SUBMISSION_REVIEW_METHOD_OPEN = 3;
public const REVIEW_ASSIGNMENT_NEW = 0; // Has never been considered by an editor, review assignment just created
public const REVIEW_ASSIGNMENT_CONSIDERED = 3; // Has been marked considered by an editor
public const REVIEW_ASSIGNMENT_UNCONSIDERED = 1; // Considered status has been revoked by an editor and is awaiting re-confirmation by an editor
public const REVIEW_ASSIGNMENT_RECONSIDERED = 2; // Considered status has been granted again by an editor
public const REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE = 0; // request has been sent but reviewer has not responded
public const REVIEW_ASSIGNMENT_STATUS_DECLINED = 1; // reviewer declined review request
public const REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE = 4; // review not responded within due date
public const REVIEW_ASSIGNMENT_STATUS_ACCEPTED = 5; // reviewer has agreed to the review
public const REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE = 6; // review not submitted within due date
public const REVIEW_ASSIGNMENT_STATUS_RECEIVED = 7; // review has been submitted
public const REVIEW_ASSIGNMENT_STATUS_COMPLETE = 8; // review has been confirmed by an editor
public const REVIEW_ASSIGNMENT_STATUS_THANKED = 9; // reviewer has been thanked
public const REVIEW_ASSIGNMENT_STATUS_CANCELLED = 10; // reviewer cancelled review request
public const REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND = 11; // request resent to reviewer after they declined
/**
* All review assignment statuses that indicate a
* review was completed
*
* @var array<int>
*/
public const REVIEW_COMPLETE_STATUSES = [
self::REVIEW_ASSIGNMENT_STATUS_RECEIVED,
self::REVIEW_ASSIGNMENT_STATUS_COMPLETE,
self::REVIEW_ASSIGNMENT_STATUS_THANKED,
];
//
// Get/set methods
//
/**
* Get ID of review assignment's submission.
*
* @return int
*/
public function getSubmissionId()
{
return $this->getData('submissionId');
}
/**
* Set ID of review assignment's submission
*
* @param int $submissionId
*/
public function setSubmissionId($submissionId)
{
$this->setData('submissionId', $submissionId);
}
/**
* Get ID of reviewer.
*
* @return int
*/
public function getReviewerId()
{
return $this->getData('reviewerId');
}
/**
* Set ID of reviewer.
*
* @param int $reviewerId
*/
public function setReviewerId($reviewerId)
{
$this->setData('reviewerId', $reviewerId);
}
/**
* Get full name of reviewer.
*
* @return string
*/
public function getReviewerFullName()
{
return $this->getData('reviewerFullName');
}
/**
* Set full name of reviewer.
*
* @param string $reviewerFullName
*/
public function setReviewerFullName($reviewerFullName)
{
$this->setData('reviewerFullName', $reviewerFullName);
}
/**
* Get reviewer comments.
*
* @return string
*/
public function getComments()
{
return $this->getData('comments');
}
/**
* Set reviewer comments.
*
* @param string $comments
*/
public function setComments($comments)
{
$this->setData('comments', $comments);
}
/**
* Get competing interests.
*
* @return string
*/
public function getCompetingInterests()
{
return $this->getData('competingInterests');
}
/**
* Set competing interests.
*
* @param string $competingInterests
*/
public function setCompetingInterests($competingInterests)
{
$this->setData('competingInterests', $competingInterests);
}
/**
* Get the workflow stage id.
*
* @return int WORKFLOW_STAGE_ID_...
*/
public function getStageId()
{
return $this->getData('stageId');
}
/**
* Set the workflow stage id.
*
* @param int $stageId WORKFLOW_STAGE_ID_...
*/
public function setStageId($stageId)
{
$this->setData('stageId', $stageId);
}
/**
* Get the method of the review (open, anonymous, or double-anonymous).
*
* @return int
*/
public function getReviewMethod()
{
return $this->getData('reviewMethod');
}
/**
* Set the type of review.
*
* @param int $method
*/
public function setReviewMethod($method)
{
$this->setData('reviewMethod', $method);
}
/**
* Get review round id.
*
* @return int
*/
public function getReviewRoundId()
{
return $this->getData('reviewRoundId');
}
/**
* Set review round id.
*
* @param int $reviewRoundId
*/
public function setReviewRoundId($reviewRoundId)
{
$this->setData('reviewRoundId', $reviewRoundId);
}
/**
* Get reviewer recommendation.
*
* @return string
*/
public function getRecommendation()
{
return $this->getData('recommendation');
}
/**
* Set reviewer recommendation.
*
* @param string $recommendation
*/
public function setRecommendation($recommendation)
{
$this->setData('recommendation', $recommendation);
}
/**
* Get considered state.
*
* @return int
*/
public function getConsidered()
{
return $this->getData('considered');
}
/**
* Set considered state.
*
* @param int $considered
*/
public function setConsidered($considered)
{
$this->setData('considered', $considered);
}
/**
* Get the date the reviewer was rated.
*
* @return string
*/
public function getDateRated()
{
return $this->getData('dateRated');
}
/**
* Set the date the reviewer was rated.
*
* @param string $dateRated
*/
public function setDateRated($dateRated)
{
$this->setData('dateRated', $dateRated);
}
/**
* Get the date of the last modification.
*
* @return string
*/
public function getLastModified()
{
return $this->getData('lastModified');
}
/**
* Set the date of the last modification.
*
* @param string $dateModified
*/
public function setLastModified($dateModified)
{
$this->setData('lastModified', $dateModified);
}
/**
* Stamp the date of the last modification to the current time.
*/
public function stampModified()
{
return $this->setLastModified(Core::getCurrentDate());
}
/**
* Get the reviewer's assigned date.
*
* @return string
*/
public function getDateAssigned()
{
return $this->getData('dateAssigned');
}
/**
* Set the reviewer's assigned date.
*
* @param string $dateAssigned
*/
public function setDateAssigned($dateAssigned)
{
$this->setData('dateAssigned', $dateAssigned);
}
/**
* Get the reviewer's notified date.
*
* @return string
*/
public function getDateNotified()
{
return $this->getData('dateNotified');
}
/**
* Set the reviewer's notified date.
*
* @param string $dateNotified
*/
public function setDateNotified($dateNotified)
{
$this->setData('dateNotified', $dateNotified);
}
/**
* Get the reviewer's confirmed date.
*
* @return string|null
*/
public function getDateConfirmed()
{
return $this->getData('dateConfirmed');
}
/**
* Set the reviewer's confirmed date.
*
* @param string|null $dateConfirmed
*/
public function setDateConfirmed($dateConfirmed)
{
$this->setData('dateConfirmed', $dateConfirmed);
}
/**
* Get the reviewer's completed date.
*
* @return string
*/
public function getDateCompleted()
{
return $this->getData('dateCompleted');
}
/**
* Set the reviewer's completed date.
*
* @param string $dateCompleted
*/
public function setDateCompleted($dateCompleted)
{
$this->setData('dateCompleted', $dateCompleted);
}
/**
* Get the reviewer's acknowledged date.
*
* @return string
*/
public function getDateAcknowledged()
{
return $this->getData('dateAcknowledged');
}
/**
* Set the reviewer's acknowledged date.
*
* @param string $dateAcknowledged
*/
public function setDateAcknowledged($dateAcknowledged)
{
$this->setData('dateAcknowledged', $dateAcknowledged);
}
/**
* Get the reviewer's last reminder date.
*
* @return string
*/
public function getDateReminded()
{
return $this->getData('dateReminded');
}
/**
* Set the reviewer's last reminder date.
*
* @param string $dateReminded
*/
public function setDateReminded($dateReminded)
{
$this->setData('dateReminded', $dateReminded);
}
/**
* Get the reviewer's due date.
*
* @return string
*/
public function getDateDue()
{
return $this->getData('dateDue');
}
/**
* Set the reviewer's due date.
*
* @param string $dateDue
*/
public function setDateDue($dateDue)
{
$this->setData('dateDue', $dateDue);
}
/**
* Get the reviewer's response due date.
*
* @return string
*/
public function getDateResponseDue()
{
return $this->getData('dateResponseDue');
}
/**
* Set the reviewer's response due date.
*
* @param string $dateResponseDue
*/
public function setDateResponseDue($dateResponseDue)
{
$this->setData('dateResponseDue', $dateResponseDue);
}
/**
* Get the declined value.
*
* @return bool
*/
public function getDeclined()
{
return $this->getData('declined');
}
/**
* Set the reviewer's declined value.
*
* @param bool $declined
*/
public function setDeclined($declined)
{
$this->setData('declined', $declined);
}
/**
* Get the cancelled value.
*
* @return bool
*/
public function getCancelled()
{
return $this->getData('cancelled');
}
/**
* Set the reviewer's cancelled value.
*
* @param bool $cancelled
*/
public function setCancelled($cancelled)
{
$this->setData('cancelled', $cancelled);
}
/**
* Get the reviewer's request resent value.
*
* @return bool
*/
public function getRequestResent()
{
return $this->getData('request_resent');
}
/**
* Set the reviewer's request resent value.
*
* @param bool $resent
*/
public function setRequestResent($resent)
{
$this->setData('request_resent', $resent);
}
/**
* Get a boolean indicating whether or not the last reminder was automatic.
*
* @return bool
*/
public function getReminderWasAutomatic()
{
return $this->getData('reminderWasAutomatic') == 1 ? 1 : 0;
}
/**
* Set the boolean indicating whether or not the last reminder was automatic.
*
* @param bool $wasAutomatic
*/
public function setReminderWasAutomatic($wasAutomatic)
{
$this->setData('reminderWasAutomatic', $wasAutomatic);
}
/**
* Get quality.
*
* @return int|null
*/
public function getQuality()
{
return $this->getData('quality');
}
/**
* Set quality.
*
* @param int|null $quality
*/
public function setQuality($quality)
{
$this->setData('quality', $quality);
}
/**
* Get round.
*
* @return int
*/
public function getRound()
{
return $this->getData('round');
}
/**
* Set round.
*
* @param int $round
*/
public function setRound($round)
{
$this->setData('round', $round);
}
/**
* Get step.
*
* @return int
*/
public function getStep()
{
return $this->getData('step');
}
/**
* Set step.
*
* @param int $step
*/
public function setStep($step)
{
$this->setData('step', $step);
}
/**
* Get review form id.
*
* @return int
*/
public function getReviewFormId()
{
return $this->getData('reviewFormId');
}
/**
* Set review form id.
*
* @param int $reviewFormId
*/
public function setReviewFormId($reviewFormId)
{
$this->setData('reviewFormId', $reviewFormId);
}
/**
* Get the current status of this review assignment
*
* @return int ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_...
*/
public function getStatus()
{
if ($this->getDeclined()) {
return self::REVIEW_ASSIGNMENT_STATUS_DECLINED;
}
if ($this->getCancelled()) {
return self::REVIEW_ASSIGNMENT_STATUS_CANCELLED;
}
if (!$this->getDeclined() && !$this->getDateConfirmed() && $this->getRequestResent()) {
return self::REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND;
}
if (!$this->getDateCompleted()) {
$dueTimes = array_map(function ($dateTime) {
// If no due time, set it to the end of the day
if (substr($dateTime, 11) === '00:00:00') {
$dateTime = substr($dateTime, 0, 11) . '23:59:59';
}
return strtotime($dateTime);
}, [$this->getDateResponseDue(), $this->getDateDue()]);
$responseDueTime = $dueTimes[0];
$reviewDueTime = $dueTimes[1];
if (!$this->getDateConfirmed()) { // no response
if ($responseDueTime < time()) { // response overdue
return self::REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE;
} elseif ($reviewDueTime < strtotime('tomorrow')) { // review overdue but not response
return self::REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE;
} else { // response not due yet
return self::REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE;
}
} else { // response given
if ($reviewDueTime < strtotime('tomorrow')) { // review due
return self::REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE;
} else {
return self::REVIEW_ASSIGNMENT_STATUS_ACCEPTED;
}
}
} elseif ($this->getDateAcknowledged()) { // reviewer thanked...
if ($this->getConsidered() == self::REVIEW_ASSIGNMENT_UNCONSIDERED) { // ...but review later unconsidered
return self::REVIEW_ASSIGNMENT_STATUS_RECEIVED;
}
return self::REVIEW_ASSIGNMENT_STATUS_THANKED;
} elseif ($this->getDateCompleted()) { // review submitted...
if ($this->getConsidered() != self::REVIEW_ASSIGNMENT_UNCONSIDERED && $this->isRead()) { // ...and confirmed by an editor
return self::REVIEW_ASSIGNMENT_STATUS_COMPLETE;
}
return self::REVIEW_ASSIGNMENT_STATUS_RECEIVED;
}
return self::REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE;
}
/**
* Determine whether an editorial user has read this review
*
* @return bool
*/
public function isRead()
{
if ($this->getConsidered() === self::REVIEW_ASSIGNMENT_CONSIDERED || $this->getConsidered() === self::REVIEW_ASSIGNMENT_RECONSIDERED) {
return true;
}
return false;
}
/**
* Get the translation key for the current status
*
* @param int $status Optionally pass a status to retrieve a specific key.
* Default will return the key for the current status.
*
* @return string
*/
public function getStatusKey($status = null)
{
if (is_null($status)) {
$status = $this->getStatus();
}
switch ($status) {
case self::REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE:
return 'submission.review.status.awaitingResponse';
case self::REVIEW_ASSIGNMENT_STATUS_CANCELLED:
return 'common.cancelled';
case self::REVIEW_ASSIGNMENT_STATUS_DECLINED:
return 'submission.review.status.declined';
case self::REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE:
return 'submission.review.status.responseOverdue';
case self::REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE:
return 'submission.review.status.reviewOverdue';
case self::REVIEW_ASSIGNMENT_STATUS_ACCEPTED:
return 'submission.review.status.accepted';
case self::REVIEW_ASSIGNMENT_STATUS_RECEIVED:
return 'submission.review.status.received';
case self::REVIEW_ASSIGNMENT_STATUS_COMPLETE:
return 'submission.review.status.complete';
case self::REVIEW_ASSIGNMENT_STATUS_THANKED:
return 'submission.review.status.thanked';
case self::REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND:
return 'submission.review.status.awaitingResponse';
}
assert(false, 'No status key could be found for ' . get_class($this) . ' on ' . __LINE__);
return '';
}
/**
* Get the translation key for the review method
*
* @param int|null $method Optionally pass a method to retrieve a specific key.
* Default will return the key for the current review method
*
* @return string
*/
public function getReviewMethodKey($method = null)
{
if (is_null($method)) {
$method = $this->getReviewMethod();
}
switch ($method) {
case self::SUBMISSION_REVIEW_METHOD_OPEN:
return 'editor.submissionReview.open';
case self::SUBMISSION_REVIEW_METHOD_ANONYMOUS:
return 'editor.submissionReview.anonymous';
case self::SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS:
return 'editor.submissionReview.doubleAnonymous';
}
assert(false, 'No review method key could be found for ' . get_class($this) . ' on ' . __LINE__);
return '';
}
//
// Files
//
/**
* Get number of weeks until review is due (or number of weeks overdue).
*
* @return ?int
*/
public function getWeeksDue()
{
$dateDue = $this->getDateDue();
if ($dateDue === null) {
return null;
}
return round((strtotime($dateDue) - time()) / (86400 * 7.0));
}
/**
* Get an associative array matching reviewer recommendation codes with locale strings.
* (Includes default '' => "Choose One" string.)
*
* @return array recommendation => localeString
*/
public static function getReviewerRecommendationOptions()
{
static $reviewerRecommendationOptions = [
'' => 'common.chooseOne',
self::SUBMISSION_REVIEWER_RECOMMENDATION_ACCEPT => 'reviewer.article.decision.accept',
self::SUBMISSION_REVIEWER_RECOMMENDATION_PENDING_REVISIONS => 'reviewer.article.decision.pendingRevisions',
self::SUBMISSION_REVIEWER_RECOMMENDATION_RESUBMIT_HERE => 'reviewer.article.decision.resubmitHere',
self::SUBMISSION_REVIEWER_RECOMMENDATION_RESUBMIT_ELSEWHERE => 'reviewer.article.decision.resubmitElsewhere',
self::SUBMISSION_REVIEWER_RECOMMENDATION_DECLINE => 'reviewer.article.decision.decline',
self::SUBMISSION_REVIEWER_RECOMMENDATION_SEE_COMMENTS => 'reviewer.article.decision.seeComments'
];
return $reviewerRecommendationOptions;
}
/**
* Return a localized string representing the reviewer recommendation.
*/
public function getLocalizedRecommendation()
{
$options = self::getReviewerRecommendationOptions();
if (array_key_exists($this->getRecommendation(), $options)) {
return __($options[$this->getRecommendation()]);
} else {
return '';
}
}
/**
* Determine if can resend request to reconsider review for this review assignment
*/
public function canResendReviewRequest(): bool
{
if ($this->getCancelled()) {
return false;
}
if (!$this->getDeclined()) {
return false;
}
return true;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewAssignment\ReviewAssignment', '\ReviewAssignment');
foreach ([
'SUBMISSION_REVIEWER_RECOMMENDATION_ACCEPT',
'SUBMISSION_REVIEWER_RECOMMENDATION_PENDING_REVISIONS',
'SUBMISSION_REVIEWER_RECOMMENDATION_RESUBMIT_HERE',
'SUBMISSION_REVIEWER_RECOMMENDATION_RESUBMIT_ELSEWHERE',
'SUBMISSION_REVIEWER_RECOMMENDATION_DECLINE',
'SUBMISSION_REVIEWER_RECOMMENDATION_SEE_COMMENTS',
'SUBMISSION_REVIEWER_RATING_VERY_GOOD',
'SUBMISSION_REVIEWER_RATING_GOOD',
'SUBMISSION_REVIEWER_RATING_AVERAGE',
'SUBMISSION_REVIEWER_RATING_POOR',
'SUBMISSION_REVIEWER_RATING_VERY_POOR',
'SUBMISSION_REVIEW_METHOD_ANONYMOUS',
'SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS',
'SUBMISSION_REVIEW_METHOD_OPEN',
'REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE',
'REVIEW_ASSIGNMENT_STATUS_DECLINED',
'REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE',
'REVIEW_ASSIGNMENT_STATUS_ACCEPTED',
'REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE',
'REVIEW_ASSIGNMENT_STATUS_RECEIVED',
'REVIEW_ASSIGNMENT_STATUS_COMPLETE',
'REVIEW_ASSIGNMENT_STATUS_THANKED',
'REVIEW_ASSIGNMENT_STATUS_CANCELLED',
'REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND',
] as $constantName) {
if (!defined($constantName)) {
define($constantName, constant('\PKP\submission\reviewAssignment\ReviewAssignment::' . $constantName));
}
}
}
@@ -0,0 +1,676 @@
<?php
/**
* @file classes/submission/reviewAssignment/ReviewAssignmentDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewAssignmentDAO
*
* @ingroup submission
*
* @see ReviewAssignment
*
* @brief Class for DAO relating reviewers to submissions.
*/
namespace PKP\submission\reviewAssignment;
use APP\core\Application;
use APP\facades\Repo;
use Exception;
use Illuminate\Support\Facades\DB;
use PKP\db\DAORegistry;
use PKP\notification\NotificationDAO;
use PKP\reviewForm\ReviewFormResponseDAO;
use PKP\submission\ReviewFilesDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
class ReviewAssignmentDAO extends \PKP\db\DAO
{
/**
* Retrieve review assignments for the passed review round id.
*
* @param int $reviewRoundId
*
* @return array
*/
public function getByReviewRoundId($reviewRoundId)
{
$params = [(int)$reviewRoundId];
$query = $this->_getSelectQuery() .
' WHERE r.review_round_id = ? ORDER BY review_id';
return $this->_getReviewAssignmentsArray($query, $params);
}
/**
* Retrieve open review assignments for the passed review round id.
*
* @param int $reviewRoundId
*
* @return array
*/
public function getOpenReviewsByReviewRoundId($reviewRoundId)
{
$params = [(int)$reviewRoundId, ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN];
$query = $this->_getSelectQuery() .
' WHERE r.review_round_id = ? AND r.review_method = ? AND r.date_confirmed IS NOT NULL AND r.declined <> 1 ORDER BY review_id';
return $this->_getReviewAssignmentsArray($query, $params);
}
/**
* Retrieve review assignments from table using the passed
* sql query and parameters.
*
* @param string $query
* @param array $queryParams
*
* @return array
*/
public function _getReviewAssignmentsArray($query, $queryParams)
{
$result = $this->retrieve($query, $queryParams);
$reviewAssignments = [];
foreach ($result as $row) {
$reviewAssignments[$row->review_id] = $this->_fromRow((array) $row);
}
return $reviewAssignments;
}
/**
* Get the review_rounds join string. Must be implemented
* by subclasses.
*
* @return string
*/
public function getReviewRoundJoin()
{
return 'r.review_round_id = r2.review_round_id';
}
//
// Public methods.
//
/**
* Retrieve a review assignment by review round and reviewer.
*
* @param int $reviewRoundId
* @param int $reviewerId
*
* @return ReviewAssignment
*/
public function getReviewAssignment($reviewRoundId, $reviewerId)
{
$result = $this->retrieve(
$this->_getSelectQuery() .
' WHERE r.review_round_id = ? AND
r.reviewer_id = ?',
[
(int) $reviewRoundId,
(int) $reviewerId
]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Retrieve a review assignment by review assignment id.
*
* @param int $reviewId
*
* @return ReviewAssignment
*/
public function getById($reviewId)
{
$result = $this->retrieve(
'SELECT r.*, r2.review_revision
FROM review_assignments r
LEFT JOIN review_rounds r2 ON (' . $this->getReviewRoundJoin() . ')
WHERE r.review_id = ?',
[(int) $reviewId]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Get all incomplete review assignments for all journals/conferences/presses
*
* @return array ReviewAssignments
*/
public function getIncompleteReviewAssignments()
{
$result = $this->retrieve(
'SELECT r.*, r2.review_revision
FROM review_assignments r
LEFT JOIN review_rounds r2 ON (' . $this->getReviewRoundJoin() . ')
WHERE' . $this->getIncompleteReviewAssignmentsWhereString() .
' ORDER BY r.submission_id'
);
$reviewAssignments = [];
foreach ($result as $row) {
$reviewAssignments[] = $this->_fromRow((array) $row);
}
return $reviewAssignments;
}
/**
* Get the WHERE sql string to filter incomplete review
* assignments.
*
* @return string
*/
public function getIncompleteReviewAssignmentsWhereString()
{
return ' r.date_notified IS NOT NULL AND
r.date_completed IS NULL AND
r.declined <> 1 AND
r.cancelled <> 1';
}
/**
* Get all review assignments for a submission.
*
* @param int $submissionId Submission ID
* @param int $reviewRoundId Review round ID
* @param int $stageId Optional stage ID
*
* @return array ReviewAssignments
*/
public function getBySubmissionId($submissionId, $reviewRoundId = null, $stageId = null)
{
$query = $this->_getSelectQuery() .
' WHERE r.submission_id = ?';
$orderBy = ' ORDER BY review_id';
$queryParams[] = (int) $submissionId;
if ($reviewRoundId != null) {
$query .= ' AND r2.review_round_id = ?';
$queryParams[] = (int) $reviewRoundId;
} else {
$orderBy .= ', r2.review_round_id';
}
if ($stageId != null) {
$query .= ' AND r2.stage_id = ?';
$queryParams[] = (int) $stageId;
} else {
$orderBy .= ', r2.stage_id';
}
$query .= $orderBy;
return $this->_getReviewAssignmentsArray($query, $queryParams);
}
/**
* Retrieve all review assignments by submission and reviewer.
*
* @param int $submissionId
* @param int $reviewerId
* @param int $stageId optional
*
* @return array
*/
public function getBySubmissionReviewer($submissionId, $reviewerId, $stageId = null)
{
$query = $this->_getSelectQuery() .
' WHERE r.submission_id = ? AND r.reviewer_id = ?';
$queryParams = [(int) $submissionId, (int) $reviewerId];
if ($stageId != null) {
$query .= ' AND r.stage_id = ?';
$queryParams[] = (int) $stageId;
}
return $this->_getReviewAssignmentsArray($query, $queryParams);
}
/**
* Get all review assignments for a reviewer.
*
* @param int $userId
*
* @return array ReviewAssignments
*/
public function getByUserId($userId)
{
$reviewRoundJoinString = $this->getReviewRoundJoin();
if (!$reviewRoundJoinString) {
throw new Exception('Review round join string not specified');
}
$result = $this->retrieve(
'SELECT r.*, r2.review_revision
FROM review_assignments r
LEFT JOIN review_rounds r2 ON (' . $reviewRoundJoinString . ')
WHERE r.reviewer_id = ?
ORDER BY round, review_id',
[(int) $userId]
);
$reviewAssignments = [];
foreach ($result as $row) {
$reviewAssignments[] = $this->_fromRow((array) $row);
}
return $reviewAssignments;
}
/**
* Check if a reviewer is assigned to a specified submission.
*
* @param int $reviewRoundId
* @param int $reviewerId
*
* @return bool
*/
public function reviewerExists($reviewRoundId, $reviewerId)
{
$result = $this->retrieve(
'SELECT COUNT(*) AS row_count
FROM review_assignments
WHERE review_round_id = ? AND
reviewer_id = ?',
[(int) $reviewRoundId, (int) $reviewerId]
);
$row = (array) $result->current();
return $row && $row['row_count'] == 1;
}
/**
* Get all review assignments for a review form.
*
* @param int $reviewFormId
*
* @return array ReviewAssignments
*/
public function getByReviewFormId($reviewFormId)
{
$reviewRoundJoinString = $this->getReviewRoundJoin();
if (!$reviewRoundJoinString) {
throw new Exception('Review round join string not specified');
}
$result = $this->retrieve(
'SELECT r.*, r2.review_revision
FROM review_assignments r
LEFT JOIN review_rounds r2 ON (' . $reviewRoundJoinString . ')
WHERE r.review_form_id = ?
ORDER BY round, review_id',
[(int) $reviewFormId]
);
$reviewAssignments = [];
foreach ($result as $row) {
$reviewAssignments[] = $this->_fromRow((array) $row);
}
return $reviewAssignments;
}
/**
* Determine the order of active reviews for the given round of the given submission
*
* @param int $submissionId Submission ID
* @param int $reviewRoundId Review round ID
*
* @return array Associating review ID with number, i.e. if review ID 26 is first returned['26']=0.
*/
public function getReviewIndexesForRound($submissionId, $reviewRoundId)
{
$result = $this->retrieve(
'SELECT review_id
FROM review_assignments
WHERE submission_id = ? AND
review_round_id = ?
ORDER BY review_id',
[(int) $submissionId, (int) $reviewRoundId]
);
$index = 0;
$returner = [];
foreach ($result as $row) {
$returner[$row->review_id] = $index++;
}
return $returner;
}
/**
* Insert a new Review Assignment.
*
* @param ReviewAssignment $reviewAssignment
*/
public function insertObject($reviewAssignment)
{
$result = $this->update(
sprintf(
'INSERT INTO review_assignments (
submission_id,
reviewer_id,
stage_id,
review_method,
round,
step,
competing_interests,
recommendation,
declined,
cancelled,
date_assigned, date_notified, date_confirmed,
date_completed, date_acknowledged, date_due, date_response_due,
quality, date_rated,
last_modified,
date_reminded, reminder_was_automatic,
review_form_id,
review_round_id,
considered,
request_resent
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, %s, %s, %s, %s, %s, %s, %s, ?, %s, %s, %s, ?, ?, ?, ?, ?
)',
$this->datetimeToDB($reviewAssignment->getDateAssigned()),
$this->datetimeToDB($reviewAssignment->getDateNotified()),
$this->datetimeToDB($reviewAssignment->getDateConfirmed()),
$this->datetimeToDB($reviewAssignment->getDateCompleted()),
$this->datetimeToDB($reviewAssignment->getDateAcknowledged()),
$this->datetimeToDB($reviewAssignment->getDateDue()),
$this->datetimeToDB($reviewAssignment->getDateResponseDue()),
$this->datetimeToDB($reviewAssignment->getDateRated()),
$this->datetimeToDB($reviewAssignment->getLastModified()),
$this->datetimeToDB($reviewAssignment->getDateReminded())
),
[
(int) $reviewAssignment->getSubmissionId(),
(int) $reviewAssignment->getReviewerId(),
(int) $reviewAssignment->getStageId(),
(int) $reviewAssignment->getReviewMethod(),
max((int) $reviewAssignment->getRound(), 1),
max((int) $reviewAssignment->getStep(), 1),
$reviewAssignment->getCompetingInterests(),
$reviewAssignment->getRecommendation(),
(int) $reviewAssignment->getDeclined(),
(int) $reviewAssignment->getCancelled(),
$reviewAssignment->getQuality(),
(int) $reviewAssignment->getReminderWasAutomatic(),
$reviewAssignment->getReviewFormId(),
(int) $reviewAssignment->getReviewRoundId(),
(int) $reviewAssignment->getConsidered(),
(int) $reviewAssignment->getRequestResent(),
]
);
$reviewAssignment->setId($this->getInsertId());
// Update review stage status whenever a review assignment is changed
$this->updateReviewRoundStatus($reviewAssignment);
}
/**
* Update an existing review assignment.
*
* @param ReviewAssignment $reviewAssignment
*/
public function updateObject($reviewAssignment)
{
$result = $this->update(
sprintf(
'UPDATE review_assignments
SET submission_id = ?,
reviewer_id = ?,
stage_id = ?,
review_method = ?,
round = ?,
step = ?,
competing_interests = ?,
recommendation = ?,
declined = ?,
cancelled = ?,
date_assigned = %s,
date_notified = %s,
date_confirmed = %s,
date_completed = %s,
date_acknowledged = %s,
date_due = %s,
date_response_due = %s,
quality = ?,
date_rated = %s,
last_modified = %s,
date_reminded = %s,
reminder_was_automatic = ?,
review_form_id = ?,
review_round_id = ?,
considered = ?,
request_resent = ?
WHERE review_id = ?',
$this->datetimeToDB($reviewAssignment->getDateAssigned()),
$this->datetimeToDB($reviewAssignment->getDateNotified()),
$this->datetimeToDB($reviewAssignment->getDateConfirmed()),
$this->datetimeToDB($reviewAssignment->getDateCompleted()),
$this->datetimeToDB($reviewAssignment->getDateAcknowledged()),
$this->datetimeToDB($reviewAssignment->getDateDue()),
$this->datetimeToDB($reviewAssignment->getDateResponseDue()),
$this->datetimeToDB($reviewAssignment->getDateRated()),
$this->datetimeToDB($reviewAssignment->getLastModified()),
$this->datetimeToDB($reviewAssignment->getDateReminded())
),
[
(int) $reviewAssignment->getSubmissionId(),
(int) $reviewAssignment->getReviewerId(),
(int) $reviewAssignment->getStageId(),
(int) $reviewAssignment->getReviewMethod(),
(int) $reviewAssignment->getRound(),
(int) $reviewAssignment->getStep(),
$reviewAssignment->getCompetingInterests(),
$reviewAssignment->getRecommendation(),
(int) $reviewAssignment->getDeclined(),
(int) $reviewAssignment->getCancelled(),
$reviewAssignment->getQuality(),
$reviewAssignment->getReminderWasAutomatic() ? 1 : 0,
$reviewAssignment->getReviewFormId(),
(int) $reviewAssignment->getReviewRoundId(),
(int) $reviewAssignment->getConsidered(),
(int) $reviewAssignment->getRequestResent(),
(int) $reviewAssignment->getId(),
]
);
// Update review stage status whenever a review assignment is changed
$this->updateReviewRoundStatus($reviewAssignment);
}
/**
* Update the status of the review round an assignment is attached to. This
* should be fired whenever a reviewer assignment is modified.
*
* @param ReviewAssignment $reviewAssignment
*/
public function updateReviewRoundStatus($reviewAssignment)
{
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getReviewRound(
$reviewAssignment->getSubmissionId(),
$reviewAssignment->getStageId(),
$reviewAssignment->getRound()
);
// Review round may not exist if submission is being deleted
if ($reviewRound) {
return $reviewRoundDao->updateStatus($reviewRound);
}
return false;
}
/**
* Internal function to return a review assignment object from a row.
*
* @param array $row
*
* @return ReviewAssignment
*/
public function _fromRow($row)
{
$reviewAssignment = $this->newDataObject(); /** @var ReviewAssignment $reviewAssignment */
$user = Repo::user()->get($row['reviewer_id'], true);
$reviewAssignment->setId((int) $row['review_id']);
$reviewAssignment->setSubmissionId((int) $row['submission_id']);
$reviewAssignment->setReviewerId((int) $row['reviewer_id']);
$reviewAssignment->setReviewerFullName($user->getFullName());
$reviewAssignment->setCompetingInterests($row['competing_interests']);
$reviewAssignment->setRecommendation($row['recommendation']);
$reviewAssignment->setDateAssigned($this->datetimeFromDB($row['date_assigned']));
$reviewAssignment->setDateNotified($this->datetimeFromDB($row['date_notified']));
$reviewAssignment->setDateConfirmed($this->datetimeFromDB($row['date_confirmed']));
$reviewAssignment->setDateCompleted($this->datetimeFromDB($row['date_completed']));
$reviewAssignment->setDateAcknowledged($this->datetimeFromDB($row['date_acknowledged']));
$reviewAssignment->setDateDue($this->datetimeFromDB($row['date_due']));
$reviewAssignment->setDateResponseDue($this->datetimeFromDB($row['date_response_due']));
$reviewAssignment->setLastModified($this->datetimeFromDB($row['last_modified']));
$reviewAssignment->setDeclined((int) $row['declined']);
$reviewAssignment->setCancelled((int) $row['cancelled']);
$reviewAssignment->setQuality($row['quality']);
$reviewAssignment->setDateRated($this->datetimeFromDB($row['date_rated']));
$reviewAssignment->setDateReminded($this->datetimeFromDB($row['date_reminded']));
$reviewAssignment->setReminderWasAutomatic((int) $row['reminder_was_automatic']);
$reviewAssignment->setRound((int) $row['round']);
$reviewAssignment->setStep((int) $row['step']);
$reviewAssignment->setReviewFormId($row['review_form_id']);
$reviewAssignment->setReviewRoundId((int) $row['review_round_id']);
$reviewAssignment->setReviewMethod((int) $row['review_method']);
$reviewAssignment->setStageId((int) $row['stage_id']);
$reviewAssignment->setConsidered((int) $row['considered']);
$reviewAssignment->setRequestResent((int) $row['request_resent'] ?? null);
return $reviewAssignment;
}
/**
* Return a new review assignment data object.
*
* @return ReviewAssignment
*/
public function newDataObject()
{
return new ReviewAssignment();
}
/**
* Delete review assignment.
*
* @param int $reviewId
*/
public function deleteById($reviewId)
{
$reviewFormResponseDao = DAORegistry::getDAO('ReviewFormResponseDAO'); /** @var ReviewFormResponseDAO $reviewFormResponseDao */
$reviewFormResponseDao->deleteByReviewId($reviewId);
$reviewFilesDao = DAORegistry::getDAO('ReviewFilesDAO'); /** @var ReviewFilesDAO $reviewFilesDao */
$reviewFilesDao->revokeByReviewId($reviewId);
$notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
$notificationDao->deleteByAssoc(Application::ASSOC_TYPE_REVIEW_ASSIGNMENT, $reviewId);
// Retrieve the review assignment before it's deleted, so it can be
// be used to fire an update on the review round status.
$reviewAssignment = $this->getById($reviewId);
$result = $this->update('DELETE FROM review_assignments WHERE review_id = ?', [(int) $reviewId]);
$this->updateReviewRoundStatus($reviewAssignment);
return $result;
}
/**
* Delete review assignments by submission ID.
*
* @param int $submissionId
*
* @return bool
*/
public function deleteBySubmissionId($submissionId)
{
$result = $this->retrieve(
'SELECT review_id FROM review_assignments WHERE submission_id = ?',
[(int) $submissionId]
);
$returner = false;
foreach ($result as $row) {
$this->deleteById($row->review_id);
$returner = true;
}
return $returner;
}
/**
* Get the last review round review assignment for a given user.
*
* @param int $submissionId
* @param int $reviewerId
*
* @return ?ReviewAssignment
*/
public function getLastReviewRoundReviewAssignmentByReviewer($submissionId, $reviewerId)
{
$result = $this->retrieve(
$this->_getSelectQuery() . ' WHERE r.submission_id = ? AND r.reviewer_id = ? ORDER BY r2.stage_id DESC, r2.round DESC',
[(int) $submissionId, (int) $reviewerId]
);
$row = (array) $result->current();
return $row ? $this->_fromRow($row) : null;
}
/**
* Return the review methods translation keys.
*
* @return array
*/
public function getReviewMethodsTranslationKeys()
{
return [
ReviewAssignment::SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS => 'editor.submissionReview.doubleAnonymous',
ReviewAssignment::SUBMISSION_REVIEW_METHOD_ANONYMOUS => 'editor.submissionReview.anonymous',
ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN => 'editor.submissionReview.open',
];
}
/**
* Get sql query to select review assignments.
*
* @return string
*/
public function _getSelectQuery()
{
return 'SELECT r.*, r2.review_revision FROM review_assignments r
LEFT JOIN review_rounds r2 ON (r.review_round_id = r2.review_round_id)';
}
/**
* Delete review assignments by review round ID.
*
* @param int $reviewRoundId
*
* @return int Number of deleted review assignments.
*/
public function deleteByReviewRoundId($reviewRoundId): int
{
return DB::table('review_assignments')
->select(['review_id', 'review_round_id'])
->where('review_round_id', $reviewRoundId)
->get()
->pluck('review_id')
->map(fn ($reviewId) => $this->deleteById($reviewId))
->count();
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewAssignment\ReviewAssignmentDAO', '\ReviewAssignmentDAO');
}
@@ -0,0 +1,354 @@
<?php
/**
* @defgroup submission_reviewRound Review Round
*/
/**
* @file classes/submission/reviewRound/ReviewRound.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewRound
*
* @ingroup submission_reviewRound
*
* @see ReviewRoundDAO
*
* @brief Basic class describing a review round.
*/
namespace PKP\submission\reviewRound;
use APP\decision\Decision;
use APP\facades\Repo;
use PKP\db\DAORegistry;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
class ReviewRound extends \PKP\core\DataObject
{
// The first four statuses are set explicitly by Decisions, which override
// the current status.
public const REVIEW_ROUND_STATUS_REVISIONS_REQUESTED = 1;
public const REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW = 2;
public const REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL = 3;
public const REVIEW_ROUND_STATUS_ACCEPTED = 4;
public const REVIEW_ROUND_STATUS_DECLINED = 5;
// The following statuses are calculated based on the statuses of ReviewAssignments
// in this round.
public const REVIEW_ROUND_STATUS_PENDING_REVIEWERS = 6; // No reviewers have been assigned
public const REVIEW_ROUND_STATUS_PENDING_REVIEWS = 7; // Waiting for reviews to be submitted by reviewers
public const REVIEW_ROUND_STATUS_REVIEWS_READY = 8; // One or more reviews is ready for an editor to view
public const REVIEW_ROUND_STATUS_REVIEWS_COMPLETED = 9; // All assigned reviews have been confirmed by an editor
public const REVIEW_ROUND_STATUS_REVIEWS_OVERDUE = 10; // One or more reviews is overdue
// The following status is calculated when the round is in ReviewRound::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED and
// at least one revision file has been uploaded.
public const REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED = 11;
// The following statuses are calculated based on the statuses of recommendOnly EditorAssignments
// and their decisions in this round.
public const REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS = 12; // Waiting for recommendations to be submitted by recommendOnly editors
public const REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY = 13; // One or more recommendations are ready for an editor to view
public const REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED = 14; // All assigned recommendOnly editors have made a recommendation
// The following status is calculated when the round is in ReviewRound::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW and
// at least one revision file has been uploaded.
public const REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED = 15;
// The following status is set when a submission return back from copyediting stage to last review round again
public const REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW = 16;
//
// Get/set methods
//
/**
* get submission id
*
* @return int
*/
public function getSubmissionId()
{
return $this->getData('submissionId');
}
/**
* set submission id
*
* @param int $submissionId
*/
public function setSubmissionId($submissionId)
{
$this->setData('submissionId', $submissionId);
}
/**
* Get review stage id (internal or external review).
*
* @return int
*/
public function getStageId()
{
return $this->getData('stageId');
}
/**
* Set review stage id
*
* @param int $stageId
*/
public function setStageId($stageId)
{
$this->setData('stageId', $stageId);
}
/**
* Get review round
*
* @return int
*/
public function getRound()
{
return $this->getData('round');
}
/**
* Set review round
*/
public function setRound($round)
{
$this->setData('round', $round);
}
/**
* Get current round status
*
* @return int
*/
public function getStatus()
{
return $this->getData('status');
}
/**
* Set current round status
*
* @param int $status
*/
public function setStatus($status)
{
$this->setData('status', $status);
}
/**
* Calculate the status of this review round.
*
* If the round is in revisions, it will search for revision files and set
* the status accordingly. If the round has not reached a revision status
* yet, it will determine the status based on the statuses of the round's
* ReviewAssignments.
*
* @return int
*/
public function determineStatus()
{
// If revisions have been requested, check to see if any have been
// submitted
if ($this->getStatus() == self::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED || $this->getStatus() == self::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED) {
// get editor decisions
$pendingRevisionDecision = Repo::decision()->getActivePendingRevisionsDecision($this->getSubmissionId(), $this->getStageId(), Decision::PENDING_REVISIONS);
if ($pendingRevisionDecision) {
if (Repo::decision()->revisionsUploadedSinceDecision($pendingRevisionDecision, $this->getSubmissionId())) {
return self::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED;
}
}
return self::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED;
}
// If revisions have been requested for re-submission, check to see if any have been
// submitted
if ($this->getStatus() == self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW || $this->getStatus() == self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED) {
// get editor decisions
$pendingRevisionDecision = Repo::decision()->getActivePendingRevisionsDecision($this->getSubmissionId(), $this->getStageId(), Decision::RESUBMIT);
if ($pendingRevisionDecision) {
if (Repo::decision()->revisionsUploadedSinceDecision($pendingRevisionDecision, $this->getSubmissionId())) {
return self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED;
}
}
return self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW;
}
$statusFinished = in_array(
$this->getStatus(),
[
self::REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL,
self::REVIEW_ROUND_STATUS_ACCEPTED,
self::REVIEW_ROUND_STATUS_DECLINED
]
);
if ($statusFinished) {
return $this->getStatus();
}
// Determine the round status by looking at the recommendOnly editor assignment statuses
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$pendingRecommendations = false;
$recommendationsFinished = true;
$recommendationsReady = false;
$editorsStageAssignments = $stageAssignmentDao->getEditorsAssignedToStage($this->getSubmissionId(), $this->getStageId());
foreach ($editorsStageAssignments as $editorsStageAssignment) {
if ($editorsStageAssignment->getRecommendOnly()) {
$pendingRecommendations = true;
// Get recommendation from the assigned recommendOnly editor
$decisions = Repo::decision()->getCollector()
->filterBySubmissionIds([$this->getSubmissionId()])
->filterByStageIds([$this->getStageId()])
->filterByReviewRoundIds([$this->getId()])
->filterByEditorIds([$editorsStageAssignment->getUserId()])
->getCount();
if (!$decisions) {
$recommendationsFinished = false;
} else {
$recommendationsReady = true;
}
}
}
if ($pendingRecommendations) {
if ($recommendationsFinished) {
return self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED;
} elseif ($recommendationsReady) {
return self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY;
}
}
// Determine the round status by looking at the assignment statuses
$anyOverdueReview = false;
$anyIncompletedReview = false;
$anyUnreadReview = false;
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignments = $reviewAssignmentDao->getByReviewRoundId($this->getId());
foreach ($reviewAssignments as $reviewAssignment) {
assert($reviewAssignment instanceof ReviewAssignment);
$assignmentStatus = $reviewAssignment->getStatus();
switch ($assignmentStatus) {
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_DECLINED:
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_CANCELLED:
break;
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE:
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE:
$anyOverdueReview = true;
break;
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE:
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_ACCEPTED:
$anyIncompletedReview = true;
break;
case ReviewAssignment::REVIEW_ASSIGNMENT_STATUS_RECEIVED:
$anyUnreadReview = true;
break;
}
}
// Find the correct review round status based on the state of
// the current review assignments. The check order matters: the
// first conditions override the others.
if (empty($reviewAssignments)) {
return self::REVIEW_ROUND_STATUS_PENDING_REVIEWERS;
} elseif ($anyOverdueReview) {
return self::REVIEW_ROUND_STATUS_REVIEWS_OVERDUE;
} elseif ($anyUnreadReview) {
return self::REVIEW_ROUND_STATUS_REVIEWS_READY;
} elseif ($anyIncompletedReview) {
return self::REVIEW_ROUND_STATUS_PENDING_REVIEWS;
} elseif ($pendingRecommendations) {
return self::REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS;
}
// The submission back form copy editing stage to last review round
if ($this->getStatus() == self::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW) {
return self::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW;
}
return self::REVIEW_ROUND_STATUS_REVIEWS_COMPLETED;
}
/**
* Get locale key associated with current status
*
* @param bool $isAuthor True iff the status is to be shown to the author (slightly tweaked phrasing)
*
* @return string
*/
public function getStatusKey($isAuthor = false)
{
switch ($this->determineStatus()) {
case self::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED:
return 'editor.submission.roundStatus.revisionsRequested';
case self::REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED:
return 'editor.submission.roundStatus.revisionsSubmitted';
case self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW:
return 'editor.submission.roundStatus.resubmitForReview';
case self::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED:
return 'editor.submission.roundStatus.submissionResubmitted';
case self::REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL:
return 'editor.submission.roundStatus.sentToExternal';
case self::REVIEW_ROUND_STATUS_ACCEPTED:
return 'editor.submission.roundStatus.accepted';
case self::REVIEW_ROUND_STATUS_DECLINED:
return 'editor.submission.roundStatus.declined';
case self::REVIEW_ROUND_STATUS_PENDING_REVIEWERS:
return 'editor.submission.roundStatus.pendingReviewers';
case self::REVIEW_ROUND_STATUS_PENDING_REVIEWS:
return 'editor.submission.roundStatus.pendingReviews';
case self::REVIEW_ROUND_STATUS_REVIEWS_READY:
return $isAuthor ? 'author.submission.roundStatus.reviewsReady' : 'editor.submission.roundStatus.reviewsReady';
case self::REVIEW_ROUND_STATUS_REVIEWS_COMPLETED:
return 'editor.submission.roundStatus.reviewsCompleted';
case self::REVIEW_ROUND_STATUS_REVIEWS_OVERDUE:
return $isAuthor ? 'author.submission.roundStatus.reviewOverdue' : 'editor.submission.roundStatus.reviewOverdue';
case self::REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS:
return 'editor.submission.roundStatus.pendingRecommendations';
case self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY:
return 'editor.submission.roundStatus.recommendationsReady';
case self::REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED:
return 'editor.submission.roundStatus.recommendationsCompleted';
case self::REVIEW_ROUND_STATUS_RETURNED_TO_REVIEW:
return 'editor.submission.roundStatus.returnedToReview';
default: return null;
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewRound\ReviewRound', '\ReviewRound');
foreach ([
'REVIEW_ROUND_STATUS_REVISIONS_REQUESTED',
'REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW',
'REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL',
'REVIEW_ROUND_STATUS_ACCEPTED',
'REVIEW_ROUND_STATUS_DECLINED',
'REVIEW_ROUND_STATUS_PENDING_REVIEWERS',
'REVIEW_ROUND_STATUS_PENDING_REVIEWS',
'REVIEW_ROUND_STATUS_REVIEWS_READY',
'REVIEW_ROUND_STATUS_REVIEWS_COMPLETED',
'REVIEW_ROUND_STATUS_REVIEWS_OVERDUE',
'REVIEW_ROUND_STATUS_REVISIONS_SUBMITTED',
'REVIEW_ROUND_STATUS_PENDING_RECOMMENDATIONS',
'REVIEW_ROUND_STATUS_RECOMMENDATIONS_READY',
'REVIEW_ROUND_STATUS_RECOMMENDATIONS_COMPLETED',
'REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW_SUBMITTED',
] as $constantName) {
define($constantName, constant('\ReviewRound::' . $constantName));
}
}
@@ -0,0 +1,400 @@
<?php
/**
* @file classes/submission/reviewRound/ReviewRoundDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewRoundDAO
*
* @ingroup submission_reviewRound
*
* @see ReviewRound
*
* @brief Operations for retrieving and modifying ReviewRound objects.
*/
namespace PKP\submission\reviewRound;
use APP\core\Application;
use Illuminate\Support\Facades\DB;
use PKP\db\DAOResultFactory;
class ReviewRoundDAO extends \PKP\db\DAO
{
//
// Public methods
//
/**
* Fetch a review round, creating it if needed.
*
* @param int $submissionId
* @param int $stageId One of the WORKFLOW_*_REVIEW_STAGE_ID constants.
* @param int $round
* @param int $status One of the ReviewRound::REVIEW_ROUND_STATUS_* constants.
*
* @return ?ReviewRound
*/
public function build($submissionId, $stageId, $round, $status = null)
{
// If one exists, fetch and return.
$reviewRound = $this->getReviewRound($submissionId, $stageId, $round);
if ($reviewRound) {
return $reviewRound;
}
// Otherwise, check the args to build one.
if ($stageId == WORKFLOW_STAGE_ID_INTERNAL_REVIEW ||
$stageId == WORKFLOW_STAGE_ID_EXTERNAL_REVIEW &&
$round > 0
) {
unset($reviewRound);
$reviewRound = $this->newDataObject();
$reviewRound->setSubmissionId($submissionId);
$reviewRound->setRound($round);
$reviewRound->setStageId($stageId);
$reviewRound->setStatus($status);
$this->insertObject($reviewRound);
$reviewRound->setId($this->getInsertId());
return $reviewRound;
} else {
assert(false);
return null;
}
}
/**
* Construct a new data object corresponding to this DAO.
*
* @return ReviewRound
*/
public function newDataObject()
{
return new ReviewRound();
}
/**
* Insert a new review round.
*
* @param ReviewRound $reviewRound
*
* @return ReviewRound
*/
public function insertObject($reviewRound)
{
$this->update(
'INSERT INTO review_rounds
(submission_id, stage_id, round, status)
VALUES
(?, ?, ?, ?)',
[
(int)$reviewRound->getSubmissionId(),
(int)$reviewRound->getStageId(),
(int)$reviewRound->getRound(),
(int)$reviewRound->getStatus()
]
);
return $reviewRound;
}
/**
* Update an existing review round.
*
* @param ReviewRound $reviewRound
*
* @return bool
*/
public function updateObject($reviewRound)
{
$returner = $this->update(
'UPDATE review_rounds
SET status = ?
WHERE submission_id = ? AND
stage_id = ? AND
round = ?',
[
(int)$reviewRound->getStatus(),
(int)$reviewRound->getSubmissionId(),
(int)$reviewRound->getStageId(),
(int)$reviewRound->getRound()
]
);
return $returner;
}
/**
* Retrieve a review round
*
* @param int $submissionId
* @param int $stageId One of the Stage_id_* constants.
* @param int $round The review round to be retrieved.
*
* @return ?ReviewRound
*/
public function getReviewRound($submissionId, $stageId, $round)
{
$result = $this->retrieve(
'SELECT * FROM review_rounds WHERE submission_id = ? AND stage_id = ? AND round = ?',
[(int) $submissionId, (int) $stageId, (int) $round]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Retrieve a review round by its id.
*
* @param int $reviewRoundId
*
* @return ReviewRound
*/
public function getById($reviewRoundId)
{
$result = $this->retrieve(
'SELECT * FROM review_rounds WHERE review_round_id = ?',
[(int) $reviewRoundId]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Retrieve a review round by a submission file id.
*
* @param int $submissionFileId
*
* @return ReviewRound
*/
public function getBySubmissionFileId($submissionFileId)
{
$result = $this->retrieve(
'SELECT * FROM review_rounds rr
INNER JOIN review_round_files rrf
ON rr.review_round_id = rrf.review_round_id
WHERE rrf.submission_file_id = ?',
[(int) $submissionFileId]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Get an iterator of review round objects associated with this submission
*
* @param int $submissionId
* @param int $stageId (optional)
* @param int $round (optional)
*
* @return DAOResultFactory<ReviewRound>
*/
public function getBySubmissionId($submissionId, $stageId = null, $round = null)
{
$params = [(int) $submissionId];
if ($stageId) {
$params[] = $stageId;
}
if ($round) {
$params[] = $round;
}
$result = $this->retrieve(
$sql = 'SELECT * FROM review_rounds WHERE submission_id = ?' .
($stageId ? ' AND stage_id = ?' : '') .
($round ? ' AND round = ?' : '') .
' ORDER BY stage_id ASC, round ASC',
$params
);
return new DAOResultFactory($result, $this, '_fromRow', [], $sql, $params);
}
/**
* Get the current review round for a given stage (or for the latest stage)
*
* @param int $submissionId
* @param int $stageId
*
* @return int
*/
public function getCurrentRoundBySubmissionId($submissionId, $stageId = null)
{
$params = [(int)$submissionId];
if ($stageId) {
$params[] = (int) $stageId;
}
$result = $this->retrieve(
'SELECT MAX(stage_id) as stage_id, MAX(round) as round
FROM review_rounds
WHERE submission_id = ?' .
($stageId ? ' AND stage_id = ?' : ''),
$params
);
$row = $result->current();
return $row ? (int) $row->round : 1;
}
/**
* Get the last review round for a give stage (or for the latest stage)
*
* @param int $submissionId
* @param int $stageId
*
* @return ?ReviewRound
*/
public function getLastReviewRoundBySubmissionId($submissionId, $stageId = null)
{
$params = [(int)$submissionId];
if ($stageId) {
$params[] = (int) $stageId;
}
$result = $this->retrieve(
'SELECT *
FROM review_rounds
WHERE submission_id = ?
' . ($stageId ? ' AND stage_id = ?' : '') . '
ORDER BY stage_id DESC, round DESC',
$params
);
$row = (array) $result->current();
return $row ? $this->_fromRow($row) : null;
}
/**
* Check if submission has a review round (in the given stage id)
*/
public function submissionHasReviewRound(int $submissionId, ?int $stageId = null): bool
{
$params = [(int)$submissionId];
if ($stageId) {
$params[] = (int) $stageId;
}
$result = $this->retrieve(
'SELECT review_round_id
FROM review_rounds
WHERE submission_id = ?
' . ($stageId ? ' AND stage_id = ?' : ''),
$params
);
return (bool) $result->current();
}
/**
* Update the review round status.
*
* @param ReviewRound $reviewRound
* @param ?int $status Optionally pass a ReviewRound::REVIEW_ROUND_STATUS_... to set a
* specific status. If not included, will determine the appropriate status
* based on ReviewRound::determineStatus().
*/
public function updateStatus($reviewRound, $status = null)
{
assert($reviewRound instanceof ReviewRound);
$currentStatus = $reviewRound->getStatus();
if (is_null($status)) {
$status = $reviewRound->determineStatus();
}
// Avoid unnecessary database access.
if ($status != $currentStatus) {
$this->update(
'UPDATE review_rounds SET status = ? WHERE review_round_id = ?',
[(int)$status, (int)$reviewRound->getId()]
);
// Update the data in object too.
$reviewRound->setStatus($status);
}
}
/**
* Delete review rounds by submission ID.
*
* @param int $submissionId
*/
public function deleteBySubmissionId($submissionId)
{
$reviewRounds = $this->getBySubmissionId($submissionId);
while ($reviewRound = $reviewRounds->next()) {
$this->deleteObject($reviewRound);
}
}
/**
* Delete a review round.
*
* @param ReviewRound $reviewRound
*/
public function deleteObject($reviewRound)
{
$this->deleteById($reviewRound->getId());
}
/**
* Delete a review round by ID.
*
* @param int $reviewRoundId
*
* @return bool
*/
public function deleteById($reviewRoundId)
{
$this->update('DELETE FROM notifications WHERE assoc_type = ? AND assoc_id = ?', [(int) Application::ASSOC_TYPE_REVIEW_ROUND, (int) $reviewRoundId]);
return $this->update('DELETE FROM review_rounds WHERE review_round_id = ?', [(int) $reviewRoundId]);
}
//
// Private methods
//
/**
* Internal function to return a review round object from a row.
*
* @param array $row
*
* @return ReviewRound
*/
public function _fromRow($row)
{
$reviewRound = $this->newDataObject();
$reviewRound->setId((int)$row['review_round_id']);
$reviewRound->setSubmissionId((int)$row['submission_id']);
$reviewRound->setStageId((int)$row['stage_id']);
$reviewRound->setRound((int)$row['round']);
$reviewRound->setStatus((int)$row['status']);
return $reviewRound;
}
/**
* Get the number of review rounds in a submission
*
* @param int $submissionId Submission id for which review round count need to be determined
* @param int $stageId Review stage id for which review round count need to be determined
*
* @throws \Exception
*
* @return int Number of internal review round associated with this submission
*
*/
public function getReviewRoundCountBySubmissionId(int $submissionId, ?int $stageId = null)
{
if (!is_null($stageId) && !in_array($stageId, [WORKFLOW_STAGE_ID_EXTERNAL_REVIEW, WORKFLOW_STAGE_ID_INTERNAL_REVIEW])) {
throw new \Exception('Not a valid review stage');
}
return DB::table('review_rounds')
->where('submission_id', $submissionId)
->when(!is_null($stageId), fn ($query) => $query->where('stage_id', $stageId))
->count();
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewRound\ReviewRoundDAO', '\ReviewRoundDAO');
}
@@ -0,0 +1,171 @@
<?php
/**
* @file classes/submission/reviewer/ReviewerAction.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewerAction
*
* @ingroup submission
*
* @brief ReviewerAction class.
*/
namespace PKP\submission\reviewer;
use APP\core\Application;
use APP\facades\Repo;
use APP\log\event\SubmissionEventLogEntry;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Illuminate\Support\Facades\Mail;
use PKP\context\Context;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\db\DAORegistry;
use PKP\log\SubmissionEmailLogDAO;
use PKP\log\SubmissionEmailLogEntry;
use PKP\mail\mailables\ReviewConfirm;
use PKP\mail\mailables\ReviewDecline;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\security\Role;
use PKP\security\Validation;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\PKPSubmission;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use Symfony\Component\Mailer\Exception\TransportException;
class ReviewerAction
{
//
// Actions.
//
/**
* Records whether the reviewer accepts the review assignment.
*/
public function confirmReview(
PKPRequest $request,
ReviewAssignment $reviewAssignment,
Submission $submission,
bool $decline,
?string $emailText = null
): void {
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewer = Repo::user()->get($reviewAssignment->getReviewerId());
if (!isset($reviewer)) {
return;
}
// Only confirm the review for the reviewer if
// he has not previously done so.
if ($reviewAssignment->getDateConfirmed() == null) {
$mailable = $this->getResponseEmail($submission, $reviewAssignment, $decline, $emailText);
Hook::call('ReviewerAction::confirmReview', [$request, $submission, $mailable, $decline]);
if (!empty($mailable->to)) {
try {
Mail::send($mailable);
$submissionEmailLogDao = DAORegistry::getDAO('SubmissionEmailLogDAO'); /** @var SubmissionEmailLogDAO $submissionEmailLogDao */
$submissionEmailLogDao->logMailable(
$decline ? SubmissionEmailLogEntry::SUBMISSION_EMAIL_REVIEW_DECLINE : SubmissionEmailLogEntry::SUBMISSION_EMAIL_REVIEW_CONFIRM,
$mailable,
$submission,
$mailable->getSenderUser()
);
} catch (TransportException $e) {
$notificationMgr = new NotificationManager();
$notificationMgr->createTrivialNotification(
$request->getUser()->getId(),
PKPNotification::NOTIFICATION_TYPE_ERROR,
['contents' => __('email.compose.error')]
);
trigger_error($e->getMessage(), E_USER_WARNING);
}
}
$reviewAssignment->setDateReminded(null);
$reviewAssignment->setReminderWasAutomatic(0);
$reviewAssignment->setDeclined($decline);
$reviewAssignment->setDateConfirmed(Core::getCurrentDate());
$reviewAssignment->stampModified();
$reviewAssignmentDao->updateObject($reviewAssignment);
// Add log
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => $decline ? SubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_DECLINE : SubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_ACCEPT,
'userId' => Validation::loggedInAs() ?? $request->getUser()->getId(),
'message' => $decline ? 'log.review.reviewDeclined' : 'log.review.reviewAccepted',
'isTranslate' => 0,
'dateLogged' => Core::getCurrentDate(),
'reviewAssignmentId' => $reviewAssignment->getId(),
'reviewerName' => $reviewer->getFullName(),
'submissionId' => $reviewAssignment->getSubmissionId(),
'round' => $reviewAssignment->getRound()
]);
Repo::eventLog()->add($eventLog);
}
}
/**
* Get the reviewer response email template.
*/
public function getResponseEmail(
PKPSubmission $submission,
ReviewAssignment $reviewAssignment,
bool $decline,
?string $emailText
): ReviewConfirm|ReviewDecline {
$context = Application::getContextDAO()->getById($submission->getData('contextId')); /** @var Context $context */
$mailable = $decline ?
new ReviewDecline($submission, $reviewAssignment, $context) :
new ReviewConfirm($submission, $reviewAssignment, $context);
// Get reviewer
$reviewer = Repo::user()->get($reviewAssignment->getReviewerId());
$mailable->sender($reviewer);
$mailable->replyTo($reviewer->getEmail(), $reviewer->getFullName());
// Get editorial contact name
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignments = $stageAssignmentDao->getBySubmissionAndStageId($submission->getId(), $reviewAssignment->getStageId());
$recipients = [];
while ($stageAssignment = $stageAssignments->next()) {
$userGroup = Repo::userGroup()->get($stageAssignment->getUserGroupId());
if (!in_array($userGroup->getRoleId(), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR])) {
continue;
}
$recipients[] = Repo::user()->get($stageAssignment->getUserId());
}
// Create dummy user if no one assigned
if (empty($recipients)) {
$contextUser = Repo::user()->getUserFromContextContact($context);
if ($contextUser->getData('email')) {
$recipients[] = $contextUser;
}
}
$mailable->recipients($recipients);
// Set email body and subject
$template = Repo::emailTemplate()->getByKey($context->getId(), $mailable->getEmailTemplateKey());
$emailText ? $mailable->body($emailText) : $mailable->body($template->getLocalizedData('body'));
$mailable->subject($template->getLocalizedData('subject'));
return $mailable;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewer\ReviewerAction', '\ReviewerAction');
}
@@ -0,0 +1,154 @@
<?php
/**
* @file classes/submission/reviewer/form/PKPReviewerReviewStep1Form.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPReviewerReviewStep1Form
*
* @ingroup submission_reviewer_form
*
* @brief Form for Step 1 of a review.
*/
namespace PKP\submission\reviewer\form;
use APP\submission\Submission;
use APP\template\TemplateManager;
use PKP\controllers\confirmationModal\linkAction\ViewCompetingInterestGuidelinesLinkAction;
use PKP\controllers\modals\review\ReviewerViewMetadataLinkAction;
use PKP\core\PKPRequest;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\AjaxModal;
use PKP\linkAction\request\ConfirmationModal;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewer\ReviewerAction;
class PKPReviewerReviewStep1Form extends ReviewerReviewForm
{
/**
* Constructor.
*/
public function __construct(PKPRequest $request, Submission $reviewSubmission, ReviewAssignment $reviewAssignment)
{
parent::__construct($request, $reviewSubmission, $reviewAssignment, 1);
$context = $request->getContext();
if (!$reviewAssignment->getDateConfirmed() && $context->getData('privacyStatement')) {
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'privacyConsent', 'required', 'user.profile.form.privacyConsentRequired'));
}
}
//
// Implement protected template methods from Form
//
/**
* @copydoc ReviewerReviewForm::fetch()
*
* @param null|mixed $template
*/
public function fetch($request, $template = null, $display = false)
{
$templateMgr = TemplateManager::getManager($request);
$context = $request->getContext();
// Add submission parameters.
$reviewAssignment = $this->getReviewAssignment();
$templateMgr->assign('reviewerCompetingInterests', $reviewAssignment->getCompetingInterests());
// Add review assignment.
$templateMgr->assign([
'reviewAssignment' => $reviewAssignment,
'reviewRoundId' => $reviewAssignment->getReviewRoundId(),
'restrictReviewerFileAccess' => $context->getData('restrictReviewerFileAccess'),
'reviewMethod' => __($reviewAssignment->getReviewMethodKey()),
]);
// Add reviewer request text.
$templateMgr->assign('reviewerRequest', __('reviewer.step1.requestBoilerplate'));
//
// Assign the link actions
//
// "View metadata" action.
$viewMetadataLinkAction = new ReviewerViewMetadataLinkAction($request, $reviewAssignment->getSubmissionId(), $reviewAssignment->getId());
$templateMgr->assign('viewMetadataAction', $viewMetadataLinkAction);
// include the confirmation modal for competing interests if the context has them.
if ($context->getLocalizedData('competingInterests') != '') {
$competingInterestsAction = new ViewCompetingInterestGuidelinesLinkAction($request);
$templateMgr->assign('competingInterestsAction', $competingInterestsAction);
}
// Instantiate the view review guidelines confirmation modal.
$aboutDueDateAction = new LinkAction(
'viewReviewGuidelines',
new ConfirmationModal(
__('reviewer.aboutDueDates.text'),
__('reviewer.aboutDueDates'),
'modal_information',
null,
'',
false
),
__('reviewer.aboutDueDates')
);
$templateMgr->assign('aboutDueDatesAction', $aboutDueDateAction);
$declineReviewLinkAction = new LinkAction(
'declineReview',
new AjaxModal(
$request->url(null, null, 'showDeclineReview', $reviewAssignment->getSubmissionId()),
__('reviewer.submission.declineReview')
)
);
$templateMgr->assign('declineReviewAction', $declineReviewLinkAction);
return parent::fetch($request, $template, $display);
}
/**
* @see Form::readInputData()
*/
public function readInputData()
{
$this->readUserVars(['competingInterestOption', 'reviewerCompetingInterests', 'privacyConsent']);
}
/**
* @see Form::execute()
*/
public function execute(...$functionParams)
{
$reviewAssignment = $this->getReviewAssignment();
$reviewSubmission = $this->getReviewSubmission();
// Set competing interests.
if ($this->getData('competingInterestOption') == 'hasCompetingInterests') {
$reviewAssignment->setCompetingInterests($this->request->getUserVar('reviewerCompetingInterests'));
} else {
$reviewAssignment->setCompetingInterests(null);
}
// Set review to next step.
$this->updateReviewStepAndSaveSubmission($reviewAssignment);
// if the reviewer has not previously confirmed the review, then
// Set that the reviewer has accepted the review.
if (!$reviewAssignment->getDateConfirmed()) {
$reviewerAction = new ReviewerAction();
$reviewerAction->confirmReview($this->request, $reviewAssignment, $reviewSubmission, false);
}
parent::execute(...$functionParams);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewer\form\PKPReviewerReviewStep1Form', '\PKPReviewerReviewStep1Form');
}
@@ -0,0 +1,73 @@
<?php
/**
* @file classes/submission/reviewer/form/PKPReviewerReviewStep2Form.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPReviewerReviewStep2Form
*
* @ingroup submission_reviewer_form
*
* @brief Form for Step 2 of a review.
*/
namespace PKP\submission\reviewer\form;
use APP\submission\Submission;
use APP\template\TemplateManager;
use PKP\core\PKPRequest;
use PKP\submission\reviewAssignment\ReviewAssignment;
class PKPReviewerReviewStep2Form extends ReviewerReviewForm
{
/**
* Constructor.
*/
public function __construct(PKPRequest $request, Submission $reviewSubmission, ReviewAssignment $reviewAssignment)
{
parent::__construct($request, $reviewSubmission, $reviewAssignment, 2);
}
//
// Implement protected template methods from Form
//
/**
* @copydoc ReviewerReviewForm::fetch()
*
* @param null|mixed $template
*/
public function fetch($request, $template = null, $display = false)
{
$templateMgr = TemplateManager::getManager($request);
$context = $this->request->getContext();
$reviewAssignment = $this->getReviewAssignment();
$reviewerGuidelines = $context->getLocalizedData($reviewAssignment->getStageId() == WORKFLOW_STAGE_ID_INTERNAL_REVIEW ? 'internalReviewGuidelines' : 'reviewGuidelines');
if (empty($reviewerGuidelines)) {
$reviewerGuidelines = __('reviewer.submission.noGuidelines');
}
$templateMgr->assign('reviewerGuidelines', $reviewerGuidelines);
return parent::fetch($request, $template, $display);
}
/**
* @see Form::execute()
*/
public function execute(...$functionParams)
{
// Set review to next step.
$this->updateReviewStepAndSaveSubmission($this->getReviewAssignment());
parent::execute(...$functionParams);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewer\form\PKPReviewerReviewStep2Form', '\PKPReviewerReviewStep2Form');
}
@@ -0,0 +1,406 @@
<?php
/**
* @file classes/submission/reviewer/form/PKPReviewerReviewStep3Form.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPReviewerReviewStep3Form
*
* @ingroup submission_reviewer_form
*
* @brief Form for Step 3 of a review.
*/
namespace PKP\submission\reviewer\form;
use APP\core\Application;
use APP\facades\Repo;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use APP\template\TemplateManager;
use Illuminate\Support\Facades\Mail;
use PKP\controllers\confirmationModal\linkAction\ViewReviewGuidelinesLinkAction;
use PKP\core\Core;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\db\DAORegistry;
use PKP\log\event\PKPSubmissionEventLogEntry;
use PKP\mail\mailables\ReviewCompleteNotifyEditors;
use PKP\notification\NotificationDAO;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\reviewForm\ReviewFormDAO;
use PKP\reviewForm\ReviewFormElement;
use PKP\reviewForm\ReviewFormElementDAO;
use PKP\reviewForm\ReviewFormResponse;
use PKP\reviewForm\ReviewFormResponseDAO;
use PKP\security\Role;
use PKP\security\Validation;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
use PKP\submission\SubmissionComment;
use PKP\submission\SubmissionCommentDAO;
class PKPReviewerReviewStep3Form extends ReviewerReviewForm
{
/**
* Constructor.
*/
public function __construct(PKPRequest $request, Submission $reviewSubmission, ReviewAssignment $reviewAssignment)
{
parent::__construct($request, $reviewSubmission, $reviewAssignment, 3);
// Validation checks for this form
$reviewFormElementDao = DAORegistry::getDAO('ReviewFormElementDAO'); /** @var ReviewFormElementDAO $reviewFormElementDao */
$requiredReviewFormElementIds = $reviewFormElementDao->getRequiredReviewFormElementIds($reviewAssignment->getReviewFormId());
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'reviewFormResponses', 'required', 'reviewer.submission.reviewFormResponse.form.responseRequired', function ($reviewFormResponses) use ($requiredReviewFormElementIds) {
foreach ($requiredReviewFormElementIds as $requiredReviewFormElementId) {
if (!isset($reviewFormResponses[$requiredReviewFormElementId]) || $reviewFormResponses[$requiredReviewFormElementId] == '') {
return false;
}
}
return true;
}));
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
}
/**
* @copydoc ReviewerReviewForm::initData
*/
public function initData()
{
$reviewAssignment = $this->getReviewAssignment();
// Retrieve most recent reviewer comments, one private, one public.
$submissionCommentDao = DAORegistry::getDAO('SubmissionCommentDAO'); /** @var SubmissionCommentDAO $submissionCommentDao */
$submissionComments = $submissionCommentDao->getReviewerCommentsByReviewerId($reviewAssignment->getSubmissionId(), $reviewAssignment->getReviewerId(), $reviewAssignment->getId(), true);
$submissionComment = $submissionComments->next();
$this->setData('comments', $submissionComment ? $submissionComment->getComments() : '');
$submissionCommentsPrivate = $submissionCommentDao->getReviewerCommentsByReviewerId($reviewAssignment->getSubmissionId(), $reviewAssignment->getReviewerId(), $reviewAssignment->getId(), false);
$submissionCommentPrivate = $submissionCommentsPrivate->next();
$this->setData('commentsPrivate', $submissionCommentPrivate ? $submissionCommentPrivate->getComments() : '');
parent::initData();
}
//
// Implement protected template methods from Form
//
/**
* @see Form::readInputData()
*/
public function readInputData()
{
$this->readUserVars(
['reviewFormResponses', 'comments', 'recommendation', 'commentsPrivate']
);
}
/**
* @copydoc ReviewerReviewForm::fetch()
*
* @param null|mixed $template
*/
public function fetch($request, $template = null, $display = false)
{
$templateMgr = TemplateManager::getManager($request);
$reviewAssignment = $this->getReviewAssignment();
// Assign the objects and data to the template.
$context = $this->request->getContext();
$templateMgr->assign([
'reviewAssignment' => $reviewAssignment,
'reviewRoundId' => $reviewAssignment->getReviewRoundId(),
'reviewerRecommendationOptions' => ReviewAssignment::getReviewerRecommendationOptions(),
]);
if ($reviewAssignment->getReviewFormId()) {
// Get the review form components
$reviewFormElementDao = DAORegistry::getDAO('ReviewFormElementDAO'); /** @var ReviewFormElementDAO $reviewFormElementDao */
$reviewFormResponseDao = DAORegistry::getDAO('ReviewFormResponseDAO'); /** @var ReviewFormResponseDAO $reviewFormResponseDao */
$reviewFormDao = DAORegistry::getDAO('ReviewFormDAO'); /** @var ReviewFormDAO $reviewFormDao */
$templateMgr->assign([
'reviewForm' => $reviewFormDao->getById($reviewAssignment->getReviewFormId(), Application::getContextAssocType(), $context->getId()),
'reviewFormElements' => $reviewFormElementDao->getByReviewFormId($reviewAssignment->getReviewFormId()),
'reviewFormResponses' => $reviewFormResponseDao->getReviewReviewFormResponseValues($reviewAssignment->getId()),
'disabled' => isset($reviewAssignment) && $reviewAssignment->getDateCompleted() != null,
]);
}
//
// Assign the link actions
//
$viewReviewGuidelinesAction = new ViewReviewGuidelinesLinkAction($request, $reviewAssignment->getStageId());
if ($viewReviewGuidelinesAction->getGuidelines()) {
$templateMgr->assign('viewGuidelinesAction', $viewReviewGuidelinesAction);
}
return parent::fetch($request, $template, $display);
}
/**
* @see Form::execute()
*/
public function execute(...$functionParams)
{
$reviewAssignment = $this->getReviewAssignment();
$notificationMgr = new NotificationManager();
// Save the answers to the review form
$this->saveReviewForm($reviewAssignment);
// Send notification
$submission = Repo::submission()->get($reviewAssignment->getSubmissionId());
$context = Application::getContextDAO()->getById($submission->getData('contextId'));
// Set review to next step.
$this->updateReviewStepAndSaveSubmission($this->getReviewAssignment());
// Mark the review assignment as completed.
$reviewAssignment->setDateCompleted(Core::getCurrentDate());
$reviewAssignment->stampModified();
// assign the recommendation to the review assignment, if there was one.
$reviewAssignment->setRecommendation((int) $this->getData('recommendation'));
// Persist the updated review assignment.
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignmentDao->updateObject($reviewAssignment);
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
$stageAssignments = $stageAssignmentDao->getBySubmissionAndStageId($submission->getId(), $submission->getStageId());
$receivedList = []; // Avoid sending twice to the same user.
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
while ($stageAssignment = $stageAssignments->next()) {
$userId = $stageAssignment->getUserId();
$userGroup = Repo::userGroup()->get($stageAssignment->getUserGroupId());
// Never send reviewer comment notification to users other than managers and editors.
if (!in_array($userGroup->getRoleId(), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]) || in_array($userId, $receivedList)) {
continue;
}
// Notify editors
$notification = $notificationMgr->createNotification(
Application::get()->getRequest(),
$userId,
PKPNotification::NOTIFICATION_TYPE_REVIEWER_COMMENT,
$submission->getContextId(),
PKPApplication::ASSOC_TYPE_REVIEW_ASSIGNMENT,
$reviewAssignment->getId()
);
// Check if user is subscribed to this type of notification emails
if (!$notification || in_array(
PKPNotification::NOTIFICATION_TYPE_REVIEWER_COMMENT,
$notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(
NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY,
$userId,
(int) $context->getId()
)
)
) {
continue;
}
$mailable = new ReviewCompleteNotifyEditors($context, $submission, $reviewAssignment);
$template = Repo::emailTemplate()->getByKey($context->getId(), ReviewCompleteNotifyEditors::getEmailTemplateKey());
// The template may not exist, see pkp/pkp-lib#9109
if (!$template) {
$template = Repo::emailTemplate()->getByKey($context->getId(), 'NOTIFICATION');
$request = Application::get()->getRequest();
$mailable->addData([
'notificationContents' => $notificationMgr->getNotificationContents($request, $notification),
'notificationUrl' => $notificationMgr->getNotificationUrl($request, $notification),
]);
}
$user = Repo::user()->get($userId);
$mailable
->from($context->getData('contactEmail'), $context->getData('contactName'))
->recipients([$user])
->subject($template->getLocalizedData('subject'))
->body($template->getLocalizedData('body'))
->allowUnsubscribe($notification);
Mail::send($mailable);
$receivedList[] = $userId;
}
// Remove the task
$notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
$notificationDao->deleteByAssoc(
PKPApplication::ASSOC_TYPE_REVIEW_ASSIGNMENT,
$reviewAssignment->getId(),
$reviewAssignment->getReviewerId(),
PKPNotification::NOTIFICATION_TYPE_REVIEW_ASSIGNMENT
);
// Add log
$reviewer = Repo::user()->get($reviewAssignment->getReviewerId(), true);
$eventLog = Repo::eventLog()->newDataObject([
'assocType' => PKPApplication::ASSOC_TYPE_SUBMISSION,
'assocId' => $submission->getId(),
'eventType' => PKPSubmissionEventLogEntry::SUBMISSION_LOG_REVIEW_READY,
'userId' => Validation::loggedInAs() ?? Application::get()->getRequest()->getUser()->getId(),
'message' => 'log.review.reviewReady',
'isTranslated' => false,
'dateLogged' => Core::getCurrentDate(),
'reviewAssignmentId' => $reviewAssignment->getId(),
'reviewerName' => $reviewer->getFullName(),
'submissionId' => $reviewAssignment->getSubmissionId(),
'round' => $reviewAssignment->getRound()
]);
Repo::eventLog()->add($eventLog);
parent::execute(...$functionParams);
}
/**
* Save the given answers for later
*/
public function saveForLater()
{
$reviewAssignment = $this->getReviewAssignment();
$notificationMgr = new NotificationManager();
// Save the answers to the review form
$this->saveReviewForm($reviewAssignment);
// Mark the review assignment as modified.
$reviewAssignment->stampModified();
// save the recommendation to the review assignment
$reviewAssignment->setRecommendation((int) $this->getData('recommendation'));
// Persist the updated review assignment.
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
$reviewAssignmentDao->updateObject($reviewAssignment);
return true;
}
/**
* Save the given answers to the review form
*
* @param ReviewAssignment $reviewAssignment
*/
public function saveReviewForm($reviewAssignment)
{
if ($reviewAssignment->getReviewFormId()) {
$reviewFormResponseDao = DAORegistry::getDAO('ReviewFormResponseDAO'); /** @var ReviewFormResponseDAO $reviewFormResponseDao */
$reviewFormResponses = $this->getData('reviewFormResponses');
if (is_array($reviewFormResponses)) {
foreach ($reviewFormResponses as $reviewFormElementId => $reviewFormResponseValue) {
$reviewFormResponse = $reviewFormResponseDao->getReviewFormResponse($reviewAssignment->getId(), $reviewFormElementId);
if (!isset($reviewFormResponse)) {
$reviewFormResponse = new ReviewFormResponse();
}
$reviewFormElementDao = DAORegistry::getDAO('ReviewFormElementDAO'); /** @var ReviewFormElementDAO $reviewFormElementDao */
$reviewFormElement = $reviewFormElementDao->getById($reviewFormElementId);
$elementType = $reviewFormElement->getElementType();
switch ($elementType) {
case ReviewFormElement::REVIEW_FORM_ELEMENT_TYPE_SMALL_TEXT_FIELD:
case ReviewFormElement::REVIEW_FORM_ELEMENT_TYPE_TEXT_FIELD:
case ReviewFormElement::REVIEW_FORM_ELEMENT_TYPE_TEXTAREA:
$reviewFormResponse->setResponseType('string');
$reviewFormResponse->setValue($reviewFormResponseValue);
break;
case ReviewFormElement::REVIEW_FORM_ELEMENT_TYPE_RADIO_BUTTONS:
case ReviewFormElement::REVIEW_FORM_ELEMENT_TYPE_DROP_DOWN_BOX:
$reviewFormResponse->setResponseType('int');
$reviewFormResponse->setValue($reviewFormResponseValue);
break;
case ReviewFormElement::REVIEW_FORM_ELEMENT_TYPE_CHECKBOXES:
$reviewFormResponse->setResponseType('object');
$reviewFormResponse->setValue($reviewFormResponseValue);
break;
}
if ($reviewFormResponse->getReviewFormElementId() != null && $reviewFormResponse->getReviewId() != null) {
$reviewFormResponseDao->updateObject($reviewFormResponse);
} else {
$reviewFormResponse->setReviewFormElementId($reviewFormElementId);
$reviewFormResponse->setReviewId($reviewAssignment->getId());
$reviewFormResponseDao->insertObject($reviewFormResponse);
}
}
}
} else {
// No review form configured. Use the default form.
if (strlen($comments = $this->getData('comments')) > 0) {
// Create a comment with the review.
$submissionCommentDao = DAORegistry::getDAO('SubmissionCommentDAO'); /** @var SubmissionCommentDAO $submissionCommentDao */
$submissionComments = $submissionCommentDao->getReviewerCommentsByReviewerId($reviewAssignment->getSubmissionId(), $reviewAssignment->getReviewerId(), $reviewAssignment->getId(), true);
$comment = $submissionComments->next();
if (!isset($comment)) {
$comment = $submissionCommentDao->newDataObject();
}
$comment->setCommentType(SubmissionComment::COMMENT_TYPE_PEER_REVIEW);
$comment->setRoleId(Role::ROLE_ID_REVIEWER);
$comment->setAssocId($reviewAssignment->getId());
$comment->setSubmissionId($reviewAssignment->getSubmissionId());
$comment->setAuthorId($reviewAssignment->getReviewerId());
$comment->setComments($comments);
$comment->setCommentTitle('');
$comment->setViewable(true);
$comment->setDatePosted(Core::getCurrentDate());
// Save or update
if ($comment->getId() != null) {
$submissionCommentDao->updateObject($comment);
} else {
$submissionCommentDao->insertObject($comment);
}
}
unset($comment);
if (strlen($commentsPrivate = $this->getData('commentsPrivate')) > 0) {
// Create a comment with the review.
$submissionCommentDao = DAORegistry::getDAO('SubmissionCommentDAO'); /** @var SubmissionCommentDAO $submissionCommentDao */
$submissionCommentsPrivate = $submissionCommentDao->getReviewerCommentsByReviewerId($reviewAssignment->getSubmissionId(), $reviewAssignment->getReviewerId(), $reviewAssignment->getId(), false);
$comment = $submissionCommentsPrivate->next();
if (!isset($comment)) {
$comment = $submissionCommentDao->newDataObject();
}
$comment->setCommentType(SubmissionComment::COMMENT_TYPE_PEER_REVIEW);
$comment->setRoleId(Role::ROLE_ID_REVIEWER);
$comment->setAssocId($reviewAssignment->getId());
$comment->setSubmissionId($reviewAssignment->getSubmissionId());
$comment->setAuthorId($reviewAssignment->getReviewerId());
$comment->setComments($commentsPrivate);
$comment->setCommentTitle('');
$comment->setViewable(false);
$comment->setDatePosted(Core::getCurrentDate());
// Save or update
if ($comment->getId() != null) {
$submissionCommentDao->updateObject($comment);
} else {
$submissionCommentDao->insertObject($comment);
}
}
unset($comment);
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewer\form\PKPReviewerReviewStep3Form', '\PKPReviewerReviewStep3Form');
}
@@ -0,0 +1,129 @@
<?php
/**
* @file classes/submission/reviewer/form/ReviewerReviewForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewerReviewForm
*
* @ingroup submission_reviewer_form
*
* @brief Base class for reviewer forms.
*/
namespace PKP\submission\reviewer\form;
use APP\submission\Submission;
use APP\template\TemplateManager;
use PKP\core\PKPRequest;
use PKP\db\DAORegistry;
use PKP\form\Form;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
class ReviewerReviewForm extends Form
{
/** @var Submission The current submission */
public Submission $_reviewSubmission;
/** @var \PKP\submission\reviewAssignment\ReviewAssignment */
public $_reviewAssignment;
/** @var int the current step */
public $_step;
/** @var PKPRequest the request object */
public $request;
/**
* Constructor.
*/
public function __construct(PKPRequest $request, Submission $reviewSubmission, ReviewAssignment $reviewAssignment, int $step)
{
parent::__construct(sprintf('reviewer/review/step%d.tpl', $step));
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
$this->request = $request;
$this->_step = (int) $step;
$this->_reviewSubmission = $reviewSubmission;
$this->_reviewAssignment = $reviewAssignment;
}
//
// Setters and Getters
//
/**
* Get the reviewer submission.
*/
public function getReviewSubmission(): Submission
{
return $this->_reviewSubmission;
}
/**
* Get the review assignment.
*/
public function getReviewAssignment(): ReviewAssignment
{
return $this->_reviewAssignment;
}
/**
* Get the review step.
*/
public function getStep(): int
{
return $this->_step;
}
//
// Implement protected template methods from Form
//
/**
* @copydoc Form::fetch()
*
* @param null|mixed $template
*/
public function fetch($request, $template = null, $display = false)
{
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign([
'submission' => $this->getReviewSubmission(),
'reviewAssignment' => $this->getReviewAssignment(),
'reviewIsClosed' => $this->getReviewAssignment()->getDateCompleted() || $this->getReviewAssignment()->getCancelled(),
'step' => $this->getStep(),
]);
return parent::fetch($request, $template, $display);
}
//
// Protected helper methods
//
/**
* Set the review step of the submission to the given
* value if it is not already set to a higher value. Then
* update the given reviewer submission.
*/
public function updateReviewStepAndSaveSubmission(ReviewAssignment $reviewAssignment)
{
// Update the review step.
$nextStep = $this->getStep() + 1;
if ($reviewAssignment->getStep() < $nextStep) {
$reviewAssignment->setStep($nextStep);
}
// Save the reviewer submission.
/** @var ReviewAssignmentDAO */
$reviewAssignmentDAO = DAORegistry::getDAO('ReviewAssignmentDAO');
$reviewAssignmentDAO->updateObject($reviewAssignment);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\submission\reviewer\form\ReviewerReviewForm', '\ReviewerReviewForm');
}