907 lines
33 KiB
PHP
907 lines
33 KiB
PHP
<?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;
|
|
}
|
|
}
|