<?php
/**
* @file controllers/grid/queries/QueriesGridHandler.php
*
* Copyright (c) 2016-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 QueriesGridHandler
*
* @ingroup controllers_grid_query
*
* @brief base PKP class to handle query grid requests.
*/
namespace PKP\controllers\grid\queries;
use APP\core\Application;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use APP\template\TemplateManager;
use Illuminate\Support\Facades\Mail;
use PKP\controllers\grid\feature\OrderGridItemsFeature;
use PKP\controllers\grid\GridColumn;
use PKP\controllers\grid\GridHandler;
use PKP\controllers\grid\queries\form\QueryForm;
use PKP\controllers\grid\queries\traits\StageMailable;
use PKP\core\JSONMessage;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\db\DAORegistry;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\AjaxModal;
use PKP\linkAction\request\RemoteActionConfirmationModal;
use PKP\log\SubmissionEmailLogDAO;
use PKP\log\SubmissionEmailLogEntry;
use PKP\notification\NotificationDAO;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\query\Query;
use PKP\query\QueryDAO;
use PKP\security\authorization\QueryAccessPolicy;
use PKP\security\authorization\QueryWorkflowStageAccessPolicy;
use PKP\security\Role;
use PKP\submissionFile\SubmissionFile;
class QueriesGridHandler extends GridHandler
{
use StageMailable;
/** @var int WORKFLOW_STAGE_ID_... */
public $_stageId;
/** @var PKPRequest */
public $_request;
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->addRoleAssignment(
[Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR],
['fetchGrid', 'fetchRow', 'readQuery', 'participants', 'addQuery', 'editQuery', 'updateQuery', 'deleteQuery']
);
$this->addRoleAssignment(
[Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
['openQuery', 'closeQuery', 'saveSequence', 'fetchTemplateBody']
);
$this->addRoleAssignment(
[Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
['leaveQuery']
);
}
//
// Getters/Setters
//
/**
* Get the authorized submission.
*
* @return Submission
*/
public function getSubmission()
{
return $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_SUBMISSION);
}
/**
* Get the authorized query.
*
* @return Query
*/
public function getQuery()
{
return $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_QUERY);
}
/**
* Get the stage id.
*
* @return int
*/
public function getStageId()
{
return $this->_stageId;
}
/**
* Get the query assoc type.
*
* @return int Application::ASSOC_TYPE_...
*/
public function getAssocType()
{
return PKPApplication::ASSOC_TYPE_SUBMISSION;
}
/**
* Get the query assoc ID.
*
* @return int
*/
public function getAssocId()
{
return $this->getSubmission()->getId();
}
/**
* Create and return a data provider for this grid.
*
* @return QueriesGridCellProvider
*/
public function getCellProvider()
{
return new QueriesGridCellProvider(
$this->getSubmission(),
$this->getStageId(),
$this->getAccessHelper()
);
}
//
// Overridden methods from PKPHandler.
// Note: this is subclassed in application-specific grids.
//
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->_stageId = (int) $request->getUserVar('stageId'); // This is being validated in WorkflowStageAccessPolicy
$this->_request = $request;
if ($request->getUserVar('queryId')) {
$this->addPolicy(new QueryAccessPolicy($request, $args, $roleAssignments, $this->_stageId));
} else {
$this->addPolicy(new QueryWorkflowStageAccessPolicy($request, $args, $roleAssignments, 'submissionId', $this->_stageId));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* @copydoc GridHandler::initialize()
*
* @param null|mixed $args
*/
public function initialize($request, $args = null)
{
parent::initialize($request, $args);
switch ($this->getStageId()) {
case WORKFLOW_STAGE_ID_SUBMISSION: $this->setTitle('submission.queries.submission');
break;
case WORKFLOW_STAGE_ID_EDITING: $this->setTitle('submission.queries.editorial');
break;
case WORKFLOW_STAGE_ID_PRODUCTION: $this->setTitle('submission.queries.production');
break;
case WORKFLOW_STAGE_ID_INTERNAL_REVIEW:
case WORKFLOW_STAGE_ID_EXTERNAL_REVIEW:
$this->setTitle('submission.queries.review');
break;
default: assert(false);
}
// Columns
$cellProvider = $this->getCellProvider();
$this->addColumn(new QueryTitleGridColumn($this->getRequestArgs()));
$this->addColumn(new GridColumn(
'from',
'submission.query.from',
null,
null,
$cellProvider,
['html' => true, 'width' => 20]
));
$this->addColumn(new GridColumn(
'lastReply',
'submission.query.lastReply',
null,
null,
$cellProvider,
['html' => true, 'width' => 20]
));
$this->addColumn(new GridColumn(
'replies',
'submission.query.replies',
null,
null,
$cellProvider,
['width' => 10, 'alignment' => GridColumn::COLUMN_ALIGNMENT_CENTER]
));
$this->addColumn(
new GridColumn(
'closed',
'submission.query.closed',
null,
'controllers/grid/common/cell/selectStatusCell.tpl',
$cellProvider,
['width' => 10, 'alignment' => GridColumn::COLUMN_ALIGNMENT_CENTER]
)
);
$router = $request->getRouter();
if ($this->getAccessHelper()->getCanCreate($this->getStageId())) {
$this->addAction(new LinkAction(
'addQuery',
new AjaxModal(
$router->url($request, null, null, 'addQuery', null, $this->getRequestArgs()),
__('grid.action.addQuery'),
'modal_add_item'
),
__('grid.action.addQuery'),
'add_item'
));
}
}
//
// Overridden methods from GridHandler
//
/**
* @copydoc GridHandler::initFeatures()
*/
public function initFeatures($request, $args)
{
$features = parent::initFeatures($request, $args);
if ($this->getAccessHelper()->getCanOrder($this->getStageId())) {
$features[] = new OrderGridItemsFeature();
}
return $features;
}
/**
* @copydoc GridHandler::getDataElementSequence()
*/
public function getDataElementSequence($row)
{
return $row->getSequence();
}
/**
* @copydoc GridHandler::setDataElementSequence()
*/
public function setDataElementSequence($request, $rowId, $gridDataElement, $newSequence)
{
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$query = $queryDao->getById($rowId, $this->getAssocType(), $this->getAssocId());
$query->setSequence($newSequence);
$queryDao->updateObject($query);
}
/**
* @copydoc GridHandler::getRowInstance()
*
* @return QueriesGridRow
*/
public function getRowInstance()
{
return new QueriesGridRow(
$this->getSubmission(),
$this->getStageId(),
$this->getAccessHelper()
);
}
/**
* Get an instance of the queries grid access helper
*
* @return QueriesAccessHelper
*/
public function getAccessHelper()
{
return new QueriesAccessHelper($this->getAuthorizedContext(), $this->_request->getUser());
}
/**
* Get the arguments that will identify the data in the grid.
* Overridden by child grids.
*
* @return array
*/
public function getRequestArgs()
{
return [
'submissionId' => $this->getSubmission()->getId(),
'stageId' => $this->getStageId(),
];
}
/**
* @copydoc GridHandler::loadData()
*
* @param null|mixed $filter
*/
public function loadData($request, $filter = null)
{
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
return $queryDao->getByAssoc(
$this->getAssocType(),
$this->getAssocId(),
$this->getStageId(),
$this->getAccessHelper()->getCanListAll($this->getStageId()) ? null : $request->getUser()->getId()
);
}
//
// Public Query Grid Actions
//
/**
* Add a query
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function addQuery($args, $request)
{
if (!$this->getAccessHelper()->getCanCreate($this->getStageId())) {
return new JSONMessage(false);
}
$queryForm = new QueryForm(
$request,
$this->getAssocType(),
$this->getAssocId(),
$this->getStageId()
);
$queryForm->initData();
return new JSONMessage(true, $queryForm->fetch($request, null, false, $this->getRequestArgs()));
}
/**
* Delete a query.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function deleteQuery($args, $request)
{
$query = $this->getQuery();
if (!$request->checkCSRF() || !$query || !$this->getAccessHelper()->getCanDelete($query->getId())) {
return new JSONMessage(false);
}
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$queryDao->deleteObject($query);
$notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
$notificationDao->deleteByAssoc(PKPApplication::ASSOC_TYPE_QUERY, $query->getId());
if ($this->getStageId() == WORKFLOW_STAGE_ID_EDITING ||
$this->getStageId() == WORKFLOW_STAGE_ID_PRODUCTION) {
// Update submission notifications
$notificationMgr = new NotificationManager();
$notificationMgr->updateNotification(
$request,
[
PKPNotification::NOTIFICATION_TYPE_ASSIGN_COPYEDITOR,
PKPNotification::NOTIFICATION_TYPE_AWAITING_COPYEDITS,
PKPNotification::NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER,
PKPNotification::NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS,
],
null,
PKPApplication::ASSOC_TYPE_SUBMISSION,
$this->getAssocId()
);
}
return \PKP\db\DAO::getDataChangedEvent($query->getId());
}
/**
* Open a closed query.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function openQuery($args, $request)
{
$query = $this->getQuery();
if (!$query || !$this->getAccessHelper()->getCanOpenClose($query)) {
return new JSONMessage(false);
}
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$query->setIsClosed(false);
$queryDao->updateObject($query);
return \PKP\db\DAO::getDataChangedEvent($query->getId());
}
/**
* Close an open query.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function closeQuery($args, $request)
{
$query = $this->getQuery();
if (!$query || !$this->getAccessHelper()->getCanOpenClose($query)) {
return new JSONMessage(false);
}
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$query->setIsClosed(true);
$queryDao->updateObject($query);
return \PKP\db\DAO::getDataChangedEvent($query->getId());
}
/**
* Get the name of the query notes grid handler.
*
* @return string
*/
public function getQueryNotesGridHandlerName()
{
return 'grid.queries.QueryNotesGridHandler';
}
/**
* Read a query
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function readQuery($args, $request)
{
$query = $this->getQuery();
$router = $request->getRouter();
$user = $request->getUser();
$context = $request->getContext();
$actionArgs = array_merge($this->getRequestArgs(), ['queryId' => $query->getId()]);
// If appropriate, create an Edit action for the participants list
if ($this->getAccessHelper()->getCanEdit($query->getId())) {
$editAction = new LinkAction(
'editQuery',
new AjaxModal(
$router->url($request, null, null, 'editQuery', null, $actionArgs),
__('grid.action.updateQuery'),
'modal_edit'
),
__('grid.action.edit'),
'edit'
);
} else {
$editAction = null;
}
$leaveQueryLinkAction = new LinkAction(
'leaveQuery',
new RemoteActionConfirmationModal(
$request->getSession(),
__('submission.query.leaveQuery.confirm'),
__('submission.query.leaveQuery'),
$router->url($request, null, null, 'leaveQuery', null, $actionArgs),
'modal_delete'
),
__('submission.query.leaveQuery'),
'leaveQuery'
);
// Show leave query button for journal managers included in the query
if ($user && $this->_getCurrentUserCanLeave($query->getId())) {
$showLeaveQueryButton = true;
} else {
$showLeaveQueryButton = false;
}
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign([
'queryNotesGridHandlerName' => $this->getQueryNotesGridHandlerName(),
'requestArgs' => $this->getRequestArgs(),
'query' => $query,
'editAction' => $editAction,
'leaveQueryLinkAction' => $leaveQueryLinkAction,
'showLeaveQueryButton' => $showLeaveQueryButton,
]);
return new JSONMessage(true, $templateMgr->fetch('controllers/grid/queries/readQuery.tpl'));
}
/**
* Fetch the list of participants for a query
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function participants($args, $request)
{
$query = $this->getQuery();
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$context = $request->getContext();
$user = $request->getUser();
$participants = [];
foreach ($queryDao->getParticipantIds($query->getId()) as $userId) {
$user = Repo::user()->get($userId);
if ($user) {
$participants[] = $user;
}
}
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign('participants', $participants);
if ($user && $this->_getCurrentUserCanLeave($query->getId())) {
$showLeaveQueryButton = true;
} else {
$showLeaveQueryButton = false;
}
$json = new JSONMessage();
$json->setStatus(true);
$json->setContent($templateMgr->fetch('controllers/grid/queries/participants.tpl'));
$json->setAdditionalAttributes(['showLeaveQueryButton' => $showLeaveQueryButton]);
return $json;
}
/**
* Edit a query
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function editQuery($args, $request)
{
$query = $this->getQuery();
if (!$this->getAccessHelper()->getCanEdit($query->getId())) {
return new JSONMessage(false);
}
// Form handling
$queryForm = new QueryForm(
$request,
$this->getAssocType(),
$this->getAssocId(),
$this->getStageId(),
$query->getId()
);
$queryForm->initData();
return new JSONMessage(true, $queryForm->fetch($request, null, false, $this->getRequestArgs()));
}
/**
* Save a query
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function updateQuery($args, $request)
{
$query = $this->getQuery();
if (!$this->getAccessHelper()->getCanEdit($query->getId())) {
return new JSONMessage(false);
}
/** @var QueryDAO */
$queryDao = DAORegistry::getDAO('QueryDAO');
$oldParticipantIds = $queryDao->getParticipantIds($query->getId());
$queryForm = new QueryForm(
$request,
$this->getAssocType(),
$this->getAssocId(),
$this->getStageId(),
$query->getId()
);
$queryForm->readInputData();
if ($queryForm->validate()) {
$queryForm->execute();
$notificationMgr = new NotificationManager();
if ($this->getStageId() == WORKFLOW_STAGE_ID_EDITING ||
$this->getStageId() == WORKFLOW_STAGE_ID_PRODUCTION) {
// Update submission notifications
$notificationMgr->updateNotification(
$request,
[
PKPNotification::NOTIFICATION_TYPE_ASSIGN_COPYEDITOR,
PKPNotification::NOTIFICATION_TYPE_AWAITING_COPYEDITS,
PKPNotification::NOTIFICATION_TYPE_ASSIGN_PRODUCTIONUSER,
PKPNotification::NOTIFICATION_TYPE_AWAITING_REPRESENTATIONS,
],
null,
PKPApplication::ASSOC_TYPE_SUBMISSION,
$this->getAssocId()
);
}
// Send notifications
$currentUser = $request->getUser();
$newParticipantIds = $queryForm->getData('users');
$added = array_diff($newParticipantIds, $oldParticipantIds);
// Don't notify the current user
if ($key = array_search($currentUser->getId(), $added)) {
unset($added[$key]);
}
/** @var NotificationSubscriptionSettingsDAO */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
$note = $query->getHeadNote();
$submission = $this->getSubmission();
// Find attachments if any
$submissionFiles = Repo::submissionFile()
->getCollector()
->filterByAssoc(
PKPApplication::ASSOC_TYPE_NOTE,
[$note->getId()]
)->filterBySubmissionIds([$submission->getId()])
->getMany();
foreach ($added as $userId) {
$user = Repo::user()->get((int) $userId);
$notification = $notificationMgr->createNotification(
$request,
$userId,
PKPNotification::NOTIFICATION_TYPE_NEW_QUERY,
$request->getContext()->getId(),
PKPApplication::ASSOC_TYPE_QUERY,
$query->getId(),
Notification::NOTIFICATION_LEVEL_TASK
);
// Check if the user is unsubscribed
$notificationSubscriptionSettings = $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(
NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY,
$user->getId(),
$request->getContext()->getId()
);
if (in_array(PKPNotification::NOTIFICATION_TYPE_NEW_QUERY, $notificationSubscriptionSettings)) {
continue;
}
$mailable = $this->getStageMailable($request->getContext(), $submission)
->sender($currentUser)
->recipients([$user])
->subject($note->getData('title'))
->body($note->getData('contents'))
->allowUnsubscribe($notification);
$submissionFiles->each(fn(SubmissionFile $item) => $mailable->attachSubmissionFile(
$item->getId(),
$item->getLocalizedData('name')
));
Mail::send($mailable);
$logDao = DAORegistry::getDAO('SubmissionEmailLogDAO'); /** @var SubmissionEmailLogDAO $logDao */
$logDao->logMailable(SubmissionEmailLogEntry::SUBMISSION_EMAIL_DISCUSSION_NOTIFY, $mailable, $submission);
}
return \PKP\db\DAO::getDataChangedEvent($query->getId());
}
// If this was new (placeholder) query that didn't validate, remember whether or not
// we need to delete it on cancellation.
if ($request->getUserVar('wasNew')) {
$queryForm->setIsNew(true);
}
return new JSONMessage(
true,
$queryForm->fetch(
$request,
null,
false,
array_merge(
$this->getRequestArgs(),
['queryId' => $query->getId()]
)
)
);
}
/**
* Leave query
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function leaveQuery($args, $request)
{
$queryId = $args['queryId'];
$user = $request->getUser();
if ($user && $this->_getCurrentUserCanLeave($queryId)) {
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$queryDao->removeParticipant($queryId, $user->getId());
$json = new JSONMessage();
$json->setEvent('user-left-discussion');
} else {
$json = new JSONMessage(false);
}
return $json;
}
/**
* Check if the current user can leave a query. Only allow if query has more than two participants.
*
* @param int $queryId
*
* @return bool
*/
public function _getCurrentUserCanLeave($queryId)
{
$userRoles = $this->getAuthorizedContextObject(PKPApplication::ASSOC_TYPE_USER_ROLES);
if (!count(array_intersect([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, ], $userRoles))) {
return false;
}
$queryDao = DAORegistry::getDAO('QueryDAO'); /** @var QueryDAO $queryDao */
$participantIds = $queryDao->getParticipantIds($queryId);
if (count($participantIds) < 3) {
return false;
}
$user = Application::get()->getRequest()->getUser();
return in_array($user->getId(), $participantIds);
}
/**
* Fetches an email template's message body.
*
* @return JSONMessage JSON object
*/
public function fetchTemplateBody(array $args, PKPRequest $request): JSONMessage
{
$templateId = $request->getUserVar('template');
$context = $request->getContext();
$template = Repo::emailTemplate()->getByKey($context->getId(), $templateId);
if ($template) {
$mailable = $this->getStageMailable($context, $this->getSubmission());
$mailable->sender($request->getUser());
$data = $mailable->getData();
return new JSONMessage(
true,
[
'body' => Mail::compileParams($template->getLocalizedData('body'), $data),
'subject' => Mail::compileParams($template->getLocalizedData('subject'), $data),
]
);
}
}
}
|