544 lines
22 KiB
PHP
544 lines
22 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file controllers/grid/queries/form/QueryForm.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 QueryForm
|
|
*
|
|
* @ingroup controllers_grid_users_queries_form
|
|
*
|
|
* @brief Form for adding/editing a new query
|
|
*/
|
|
|
|
namespace PKP\controllers\grid\queries\form;
|
|
|
|
use APP\core\Application;
|
|
use APP\core\Request;
|
|
use APP\facades\Repo;
|
|
use APP\template\TemplateManager;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use PKP\controllers\grid\queries\traits\StageMailable;
|
|
use PKP\core\Core;
|
|
use PKP\core\PKPApplication;
|
|
use PKP\core\PKPRequest;
|
|
use PKP\db\DAORegistry;
|
|
use PKP\form\Form;
|
|
use PKP\note\NoteDAO;
|
|
use PKP\notification\NotificationDAO;
|
|
use PKP\query\Query;
|
|
use PKP\query\QueryDAO;
|
|
use PKP\security\Role;
|
|
use PKP\stageAssignment\StageAssignmentDAO;
|
|
use PKP\submission\reviewAssignment\ReviewAssignment;
|
|
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
|
|
|
|
class QueryForm extends Form
|
|
{
|
|
use StageMailable;
|
|
|
|
/** @var int Application::ASSOC_TYPE_... */
|
|
public $_assocType;
|
|
|
|
/** @var int Assoc ID (per _assocType) */
|
|
public $_assocId;
|
|
|
|
/** @var int The stage id associated with the query being edited */
|
|
public $_stageId;
|
|
|
|
/** @var Query The query being edited */
|
|
public $_query;
|
|
|
|
/** @var bool True iff this is a newly-created query */
|
|
public $_isNew;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param Request $request
|
|
* @param int $assocType Application::ASSOC_TYPE_...
|
|
* @param int $assocId Assoc ID (per assocType)
|
|
* @param int $stageId WORKFLOW_STAGE_...
|
|
* @param int $queryId Optional query ID to edit. If none provided, a
|
|
* (potentially temporary) query will be created.
|
|
*/
|
|
public function __construct($request, $assocType, $assocId, $stageId, $queryId = null)
|
|
{
|
|
parent::__construct('controllers/grid/queries/form/queryForm.tpl');
|
|
$this->setStageId($stageId);
|
|
|
|
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
|
|
if (!$queryId) {
|
|
$this->_isNew = true;
|
|
|
|
// Create a query
|
|
$query = $queryDao->newDataObject();
|
|
$query->setAssocType($assocType);
|
|
$query->setAssocId($assocId);
|
|
$query->setStageId($stageId);
|
|
$query->setSequence(REALLY_BIG_NUMBER);
|
|
$queryDao->insertObject($query);
|
|
$queryDao->resequence($assocType, $assocId);
|
|
|
|
// Add the current user as a participant by default.
|
|
$queryDao->insertParticipant($query->getId(), $request->getUser()->getId());
|
|
|
|
// Create a head note
|
|
$noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
|
|
$headNote = $noteDao->newDataObject();
|
|
$headNote->setUserId($request->getUser()->getId());
|
|
$headNote->setAssocType(Application::ASSOC_TYPE_QUERY);
|
|
$headNote->setAssocId($query->getId());
|
|
$headNote->setDateCreated(Core::getCurrentDate());
|
|
$noteDao->insertObject($headNote);
|
|
} else {
|
|
$query = $queryDao->getById($queryId, $assocType, $assocId);
|
|
assert(isset($query));
|
|
// New queries will not have a head note.
|
|
$this->_isNew = !$query->getHeadNote();
|
|
}
|
|
|
|
$this->setQuery($query);
|
|
|
|
// Validation checks for this form
|
|
$this->addCheck(new \PKP\form\validation\FormValidatorCustom($this, 'users', 'required', 'stageParticipants.notify.warning', function ($users) {
|
|
return count($users) > 1;
|
|
}));
|
|
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'subject', 'required', 'submission.queries.subjectRequired'));
|
|
$this->addCheck(new \PKP\form\validation\FormValidator($this, 'comment', 'required', 'submission.queries.messageRequired'));
|
|
$this->addCheck(new \PKP\form\validation\FormValidatorPost($this));
|
|
$this->addCheck(new \PKP\form\validation\FormValidatorCSRF($this));
|
|
}
|
|
|
|
//
|
|
// Getters and Setters
|
|
//
|
|
/**
|
|
* Set the flag indicating whether the query is new (i.e. creates a placeholder that needs deleting on cancel)
|
|
*
|
|
*/
|
|
public function setIsNew(bool $isNew)
|
|
{
|
|
$this->_isNew = $isNew;
|
|
}
|
|
|
|
/**
|
|
* Get the query
|
|
*
|
|
* @return Query
|
|
*/
|
|
public function getQuery()
|
|
{
|
|
return $this->_query;
|
|
}
|
|
|
|
/**
|
|
* Set the query
|
|
*
|
|
* @param Query $query
|
|
*/
|
|
public function setQuery($query)
|
|
{
|
|
$this->_query = $query;
|
|
}
|
|
|
|
/**
|
|
* Get the stage id
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getStageId()
|
|
{
|
|
return $this->_stageId;
|
|
}
|
|
|
|
/**
|
|
* Set the stage id
|
|
*
|
|
* @param int $stageId
|
|
*/
|
|
public function setStageId($stageId)
|
|
{
|
|
$this->_stageId = $stageId;
|
|
}
|
|
|
|
/**
|
|
* Get assoc type
|
|
*
|
|
* @return int Application::ASSOC_TYPE_...
|
|
*/
|
|
public function getAssocType()
|
|
{
|
|
return $this->getData('assocType');
|
|
}
|
|
|
|
/**
|
|
* Set assoc type
|
|
*
|
|
* @param int $assocType Application::ASSOC_TYPE_...
|
|
*/
|
|
public function setAssocType($assocType)
|
|
{
|
|
$this->setData('assocType', $assocType);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
|
|
//
|
|
// Overridden template methods
|
|
//
|
|
/**
|
|
* Initialize form data from the associated author.
|
|
*/
|
|
public function initData()
|
|
{
|
|
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
|
|
if ($query = $this->getQuery()) {
|
|
$headNote = $query->getHeadNote();
|
|
$this->_data = [
|
|
'queryId' => $query->getId(),
|
|
'subject' => $headNote ? $headNote->getTitle() : null,
|
|
'comment' => $headNote ? $headNote->getContents() : null,
|
|
'userIds' => $queryDao->getParticipantIds($query->getId()),
|
|
'template' => null,
|
|
];
|
|
} else {
|
|
// set initial defaults for queries.
|
|
}
|
|
// in order to be able to use the hook
|
|
return parent::initData();
|
|
}
|
|
|
|
/**
|
|
* Fetch the form.
|
|
*
|
|
* @see Form::fetch()
|
|
*
|
|
* @param PKPRequest $request
|
|
* @param array $actionArgs Optional list of additional arguments
|
|
* @param null|mixed $template
|
|
*/
|
|
public function fetch($request, $template = null, $display = false, $actionArgs = [])
|
|
{
|
|
$query = $this->getQuery();
|
|
$headNote = $query->getHeadNote();
|
|
$user = $request->getUser();
|
|
$context = $request->getContext();
|
|
|
|
$templateMgr = TemplateManager::getManager($request);
|
|
$templateMgr->assign([
|
|
'isNew' => $this->_isNew,
|
|
'noteId' => $headNote->getId(),
|
|
'actionArgs' => $actionArgs,
|
|
'csrfToken' => $request->getSession()->getCSRFToken(),
|
|
'stageId' => $this->getStageId(),
|
|
'assocId' => $query->getAssocId(),
|
|
'assocType' => $query->getAssocType(),
|
|
]);
|
|
|
|
// Queries only support Application::ASSOC_TYPE_SUBMISSION so far
|
|
if ($query->getAssocType() !== PKPApplication::ASSOC_TYPE_SUBMISSION) {
|
|
return parent::fetch($request, $template, $display);
|
|
}
|
|
|
|
$submission = Repo::submission()->get($query->getAssocId());
|
|
|
|
// Add the templates that can be used for this discussion
|
|
$templateKeySubjectPairs = [];
|
|
if ($user->hasRole([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT], $context->getId())) {
|
|
$mailable = $this->getStageMailable($context, $submission);
|
|
$data = $mailable->getData();
|
|
$defaultTemplate = Repo::emailTemplate()->getByKey($context->getId(), $mailable::getEmailTemplateKey());
|
|
$templateKeySubjectPairs = [$mailable::getEmailTemplateKey() => $defaultTemplate->getLocalizedData('name')];
|
|
$alternateTemplates = Repo::emailTemplate()->getCollector($context->getId())
|
|
->alternateTo([$mailable::getEmailTemplateKey()])
|
|
->getMany();
|
|
foreach ($alternateTemplates as $alternateTemplate) {
|
|
$templateKeySubjectPairs[$alternateTemplate->getData('key')] = Mail::compileParams(
|
|
$alternateTemplate->getLocalizedData('name'),
|
|
$data
|
|
);
|
|
}
|
|
}
|
|
|
|
$templateMgr->assign('templates', $templateKeySubjectPairs);
|
|
|
|
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
|
|
|
|
// Get currently selected participants in the query
|
|
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
|
|
$assignedParticipants = $query->getId() ? $queryDao->getParticipantIds($query->getId()) : [];
|
|
|
|
// Always include current user, even if not with a stage assignment
|
|
$includeUsers[] = $user->getId();
|
|
$excludeUsers = null;
|
|
|
|
// When in review stage, include/exclude users depending on the current users role
|
|
$reviewAssignments = [];
|
|
// Get current users roles
|
|
$assignedRoles = (function () use ($stageAssignmentDao, $query, $user) {
|
|
$assignedRoles = [];
|
|
$usersAssignments = $stageAssignmentDao->getBySubmissionAndStageId($query->getAssocId(), $query->getStageId(), null, $user->getId());
|
|
while ($usersAssignment = $usersAssignments->next()) {
|
|
$userGroup = Repo::userGroup()->get($usersAssignment->getUserGroupId());
|
|
$assignedRoles[] = $userGroup->getRoleId();
|
|
}
|
|
return $assignedRoles;
|
|
})();
|
|
|
|
if ($query->getStageId() == WORKFLOW_STAGE_ID_EXTERNAL_REVIEW || $query->getStageId() == WORKFLOW_STAGE_ID_INTERNAL_REVIEW) {
|
|
// Get all review assignments for current submission
|
|
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
|
|
$reviewAssignments = $reviewAssignmentDao->getBySubmissionId($submission->getId());
|
|
|
|
// if current user is editor/journal manager/site admin and not have author role , add all reviewers
|
|
if (array_intersect($assignedRoles, [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]) || (empty($assignedRoles) && ($user->hasRole([Role::ROLE_ID_MANAGER], $context->getId()) || $user->hasRole([Role::ROLE_ID_SITE_ADMIN], PKPApplication::CONTEXT_SITE)))) {
|
|
foreach ($reviewAssignments as $reviewAssignment) {
|
|
$includeUsers[] = $reviewAssignment->getReviewerId();
|
|
}
|
|
}
|
|
|
|
// if current user is an anonymous reviewer, filter out authors
|
|
foreach ($reviewAssignments as $reviewAssignment) {
|
|
if ($reviewAssignment->getReviewerId() == $user->getId()) {
|
|
if ($reviewAssignment->getReviewMethod() != ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN) {
|
|
$authorAssignments = $stageAssignmentDao->getBySubmissionAndRoleId($query->getAssocId(), Role::ROLE_ID_AUTHOR);
|
|
while ($assignment = $authorAssignments->next()) {
|
|
$excludeUsers[] = $assignment->getUserId();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if current user is author, add open reviewers who have accepted the request
|
|
if (array_intersect([Role::ROLE_ID_AUTHOR], $assignedRoles)) {
|
|
foreach ($reviewAssignments as $reviewAssignment) {
|
|
if ($reviewAssignment->getReviewMethod() == ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN && $reviewAssignment->getDateConfirmed() && !$reviewAssignment->getDeclined()) {
|
|
$includeUsers[] = $reviewAssignment->getReviewerId();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$usersIterator = Repo::user()->getCollector()
|
|
->filterByContextIds([$context->getId()])
|
|
->limit(100)
|
|
->offset(0)
|
|
->assignedTo($query->getAssocId(), $query->getStageId())
|
|
->excludeUserIds($excludeUsers)
|
|
->getMany();
|
|
|
|
$includedUsersIterator = Repo::user()->getCollector()->filterByUserIds($includeUsers)->getMany();
|
|
$usersIterator = $usersIterator->merge($includedUsersIterator);
|
|
|
|
$allParticipants = [];
|
|
foreach ($usersIterator as $user) {
|
|
$allUserGroups = Repo::userGroup()->userUserGroups($user->getId(), $context->getId());
|
|
|
|
$userRoles = [];
|
|
$userAssignments = $stageAssignmentDao->getBySubmissionAndStageId($query->getAssocId(), $query->getStageId(), null, $user->getId())->toArray();
|
|
foreach ($userAssignments as $userAssignment) {
|
|
foreach ($allUserGroups as $userGroup) {
|
|
if ($userGroup->getId() == $userAssignment->getUserGroupId()) {
|
|
$userRoles[] = $userGroup->getLocalizedName();
|
|
}
|
|
}
|
|
}
|
|
foreach ($reviewAssignments as $assignment) {
|
|
if ($assignment->getReviewerId() == $user->getId()) {
|
|
$userRoles[] = __('user.role.reviewer') . ' (' . __($assignment->getReviewMethodKey()) . ')';
|
|
}
|
|
}
|
|
if (!count($userRoles)) {
|
|
$userRoles[] = __('submission.status.unassigned');
|
|
}
|
|
$allParticipants[$user->getId()] = __('submission.query.participantTitle', [
|
|
'fullName' => $user->getFullName(),
|
|
'userGroup' => join(__('common.commaListSeparator'), $userRoles),
|
|
]);
|
|
}
|
|
|
|
// Notify assistants, authors and reviewers that they have x minutes to update their own discussion
|
|
$allowedEditTimeNotice = ['show' => false, 'limit' => 60];
|
|
if (array_intersect($assignedRoles, [Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR, Role::ROLE_ID_REVIEWER])) {
|
|
$allowedEditTimeNotice['show'] = true;
|
|
$allowedEditTimeNotice['limit'] = (int) ($allowedEditTimeNotice['limit'] - (time() - strtotime($headNote->getDateCreated())) / 60);
|
|
}
|
|
|
|
$templateMgr->assign([
|
|
'allParticipants' => $allParticipants,
|
|
'assignedParticipants' => $assignedParticipants,
|
|
'allowedEditTimeNotice' => $allowedEditTimeNotice,
|
|
]);
|
|
|
|
return parent::fetch($request, $template, $display);
|
|
}
|
|
|
|
/**
|
|
* Assign form data to user-submitted data.
|
|
*
|
|
* @see Form::readInputData()
|
|
*/
|
|
public function readInputData()
|
|
{
|
|
$this->readUserVars([
|
|
'subject',
|
|
'comment',
|
|
'users',
|
|
'template',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @copydoc Form::validate()
|
|
*/
|
|
public function validate($callHooks = true)
|
|
{
|
|
// Display error if anonymity is impacted in a review stage:
|
|
// 1) several blind reviewers are selected, or
|
|
// 2) a blind reviewer and another participant (other than editor or assistant) are selected.
|
|
// Editors and assistants are ignored, they can see everything.
|
|
// Also admin and manager, if they are creating the discussion, are ignored -- they can see everything.
|
|
// In other stages validate that participants are assigned to that stage.
|
|
$query = $this->getQuery();
|
|
// Queries only support Application::ASSOC_TYPE_SUBMISSION so far (see above)
|
|
if ($query->getAssocType() == Application::ASSOC_TYPE_SUBMISSION) {
|
|
$request = Application::get()->getRequest();
|
|
$user = $request->getUser();
|
|
$context = $request->getContext();
|
|
$submissionId = $query->getAssocId();
|
|
$stageId = $query->getStageId();
|
|
|
|
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO'); /** @var ReviewAssignmentDAO $reviewAssignmentDao */
|
|
$stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO'); /** @var StageAssignmentDAO $stageAssignmentDao */
|
|
|
|
// get the selected participants
|
|
$newParticipantIds = (array) $this->getData('users');
|
|
$participantsToConsider = $blindReviewerCount = 0;
|
|
foreach ($newParticipantIds as $participantId) {
|
|
// get participant roles in this workflow stage
|
|
$assignedRoles = [];
|
|
$usersAssignments = $stageAssignmentDao->getBySubmissionAndStageId($submissionId, $stageId, null, $participantId);
|
|
while ($usersAssignment = $usersAssignments->next()) {
|
|
$userGroup = Repo::userGroup()->get($usersAssignment->getUserGroupId());
|
|
$assignedRoles[] = $userGroup->getRoleId();
|
|
}
|
|
|
|
if ($stageId == WORKFLOW_STAGE_ID_EXTERNAL_REVIEW || $stageId == WORKFLOW_STAGE_ID_INTERNAL_REVIEW) {
|
|
// validate the anonymity
|
|
// get participant review assignments
|
|
$reviewAssignments = $reviewAssignmentDao->getBySubmissionReviewer($submissionId, $participantId, $stageId);
|
|
// if participant has no role in this stage and is not a reviewer
|
|
if (empty($assignedRoles) && empty($reviewAssignments)) {
|
|
// if participant is current user and the user has admin or manager role, ignore participant
|
|
if (($participantId == $user->getId()) && ($user->hasRole([Role::ROLE_ID_SITE_ADMIN], PKPApplication::CONTEXT_SITE) || $user->hasRole([Role::ROLE_ID_MANAGER], $context->getId()))) {
|
|
continue;
|
|
} else {
|
|
$this->addError('users', __('editor.discussion.errorNotStageParticipant'));
|
|
$this->addErrorField('users');
|
|
break;
|
|
}
|
|
}
|
|
// is participant a blind reviewer
|
|
$blindReviewer = false;
|
|
foreach ($reviewAssignments as $reviewAssignment) {
|
|
if ($reviewAssignment->getReviewMethod() != ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN) {
|
|
$blindReviewerCount++;
|
|
$blindReviewer = true;
|
|
break;
|
|
}
|
|
}
|
|
// if participant is not a blind reviewer and has a role different than editor or assistant
|
|
if (!$blindReviewer && !array_intersect([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT], $assignedRoles)) {
|
|
$participantsToConsider++;
|
|
}
|
|
// if anonymity is impacted, display error
|
|
if (($blindReviewerCount > 1) || ($blindReviewerCount > 0 && $participantsToConsider > 0)) {
|
|
$this->addError('users', __('editor.discussion.errorAnonymousParticipants'));
|
|
$this->addErrorField('users');
|
|
break;
|
|
}
|
|
} else {
|
|
// if participant has no role/assignment in the current stage
|
|
if (empty($assignedRoles)) {
|
|
// if participant is current user and the user has admin or manager role, ignore participant
|
|
if (($participantId == $user->getId()) && ($user->hasRole([Role::ROLE_ID_SITE_ADMIN], PKPApplication::CONTEXT_SITE) || $user->hasRole([Role::ROLE_ID_MANAGER], $context->getId()))) {
|
|
continue;
|
|
} else {
|
|
$this->addError('users', __('editor.discussion.errorNotStageParticipant'));
|
|
$this->addErrorField('users');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return parent::validate($callHooks);
|
|
}
|
|
|
|
/**
|
|
* @copydoc Form::execute()
|
|
*/
|
|
public function execute(...$functionArgs)
|
|
{
|
|
$request = Application::get()->getRequest();
|
|
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
|
|
$query = $this->getQuery();
|
|
|
|
$headNote = $query->getHeadNote();
|
|
$headNote->setTitle($this->getData('subject'));
|
|
$headNote->setContents($this->getData('comment'));
|
|
|
|
$noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
|
|
$noteDao->updateObject($headNote);
|
|
|
|
$queryDao->updateObject($query);
|
|
|
|
// Update participants
|
|
$oldParticipantIds = $queryDao->getParticipantIds($query->getId());
|
|
$newParticipantIds = $this->getData('users');
|
|
$queryDao->removeAllParticipants($query->getId());
|
|
foreach ($newParticipantIds as $userId) {
|
|
$queryDao->insertParticipant($query->getId(), $userId);
|
|
}
|
|
|
|
$removed = array_diff($oldParticipantIds, $newParticipantIds);
|
|
foreach ($removed as $userId) {
|
|
// Delete this users' notifications relating to this query
|
|
$notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
|
|
$notificationDao->deleteByAssoc(Application::ASSOC_TYPE_QUERY, $query->getId(), $userId);
|
|
}
|
|
|
|
// Stamp the submission status modification date.
|
|
if ($query->getAssocType() == Application::ASSOC_TYPE_SUBMISSION) {
|
|
$submission = Repo::submission()->get($query->getAssocId());
|
|
$submission->stampLastActivity();
|
|
Repo::submission()->dao->update($submission);
|
|
}
|
|
|
|
parent::execute(...$functionArgs);
|
|
}
|
|
}
|