611 lines
21 KiB
PHP
611 lines
21 KiB
PHP
<?php
|
||
|
||
/**
|
||
* @file pages/stats/PKPStatsHandler.php
|
||
*
|
||
* Copyright (c) 2013-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 PKPStatsHandler
|
||
*
|
||
* @ingroup pages_stats
|
||
*
|
||
* @brief Handle requests for statistics pages.
|
||
*/
|
||
|
||
namespace PKP\pages\stats;
|
||
|
||
use APP\core\Application;
|
||
use APP\core\Request;
|
||
use APP\core\Services;
|
||
use APP\facades\Repo;
|
||
use APP\handler\Handler;
|
||
use APP\template\TemplateManager;
|
||
use PKP\core\PKPApplication;
|
||
use PKP\core\PKPRequest;
|
||
use PKP\plugins\PluginRegistry;
|
||
use PKP\security\authorization\ContextAccessPolicy;
|
||
use PKP\security\Role;
|
||
use PKP\statistics\PKPStatisticsHelper;
|
||
use PKP\sushi\CounterR5Report;
|
||
|
||
class PKPStatsHandler extends Handler
|
||
{
|
||
/** @copydoc PKPHandler::_isBackendPage */
|
||
public $_isBackendPage = true;
|
||
|
||
/**
|
||
* Constructor.
|
||
*/
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
$this->addRoleAssignment(
|
||
[Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
|
||
['editorial', 'publications', 'context', 'users', 'reports', 'counterR5']
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @see PKPHandler::authorize()
|
||
*
|
||
* @param PKPRequest $request
|
||
* @param array $args
|
||
* @param array $roleAssignments
|
||
*/
|
||
public function authorize($request, &$args, $roleAssignments)
|
||
{
|
||
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
|
||
return parent::authorize($request, $args, $roleAssignments);
|
||
}
|
||
|
||
|
||
//
|
||
// Public handler methods.
|
||
//
|
||
/**
|
||
* Display editorial stats about the submission workflow process
|
||
*
|
||
* @param array $args
|
||
* @param Request $request
|
||
*/
|
||
public function editorial($args, $request)
|
||
{
|
||
$dispatcher = $request->getDispatcher();
|
||
$context = $request->getContext();
|
||
|
||
if (!$context) {
|
||
$dispatcher->handle404();
|
||
}
|
||
|
||
$templateMgr = TemplateManager::getManager($request);
|
||
$this->setupTemplate($request);
|
||
|
||
$dateStart = date('Y-m-d', strtotime('-91 days'));
|
||
$dateEnd = date('Y-m-d', strtotime('yesterday'));
|
||
|
||
$args = [
|
||
'contextIds' => [$context->getId()],
|
||
];
|
||
|
||
$totals = Services::get('editorialStats')->getOverview($args);
|
||
$averages = Services::get('editorialStats')->getAverages($args);
|
||
$dateRangeTotals = Services::get('editorialStats')->getOverview(
|
||
array_merge(
|
||
$args,
|
||
[
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
]
|
||
)
|
||
);
|
||
|
||
// Stats that should be converted to percentages
|
||
$percentageStats = [
|
||
'acceptanceRate',
|
||
'declineRate',
|
||
'declinedDeskRate',
|
||
'declinedReviewRate',
|
||
];
|
||
|
||
// Stats that should be indented in the table
|
||
$indentStats = [
|
||
'submissionsDeclinedDeskReject',
|
||
'submissionsDeclinedPostReview',
|
||
'daysToAccept',
|
||
'daysToReject',
|
||
'declinedDeskRate',
|
||
'declinedReviewRate',
|
||
'submissionsInProgress',
|
||
'submissionsImported',
|
||
];
|
||
|
||
// Compile table rows
|
||
$tableRows = [];
|
||
foreach ($totals as $i => $stat) {
|
||
$row = [
|
||
'key' => $stat['key'],
|
||
'name' => __($stat['name']),
|
||
'total' => $stat['value'],
|
||
'dateRange' => $dateRangeTotals[$i]['value'],
|
||
];
|
||
if (in_array($stat['key'], $indentStats)) {
|
||
$row['name'] = ' ' . $row['name'];
|
||
}
|
||
if (in_array($stat['key'], $percentageStats)) {
|
||
$row['total'] = ($stat['value'] * 100) . '%';
|
||
$row['dateRange'] = ($dateRangeTotals[$i]['value'] * 100) . '%';
|
||
}
|
||
$description = $this->_getStatDescription($stat['key']);
|
||
if ($description) {
|
||
$row['description'] = $description;
|
||
}
|
||
if (array_key_exists($stat['key'], $averages)
|
||
&& $averages[$stat['key']] !== -1
|
||
&& $row['total'] > 0) {
|
||
$row['total'] = __('stats.countWithYearlyAverage', [
|
||
'count' => $stat['value'],
|
||
'average' => $averages[$stat['key']],
|
||
]);
|
||
}
|
||
$tableRows[] = $row;
|
||
}
|
||
|
||
// Get the workflow stage counts
|
||
$activeByStage = [];
|
||
foreach (Application::getApplicationStages() as $stageId) {
|
||
$activeByStage[] = [
|
||
'name' => __(Application::getWorkflowStageName($stageId)),
|
||
'count' => Services::get('editorialStats')->countActiveByStages($stageId, $args),
|
||
'color' => Application::getWorkflowStageColor($stageId),
|
||
];
|
||
}
|
||
|
||
$statsComponent = new \PKP\components\PKPStatsEditorialPage(
|
||
$dispatcher->url($request, PKPApplication::ROUTE_API, $context->getPath(), 'stats/editorial'),
|
||
[
|
||
'activeByStage' => $activeByStage,
|
||
'averagesApiUrl' => $dispatcher->url($request, PKPApplication::ROUTE_API, $context->getPath(), 'stats/editorial/averages'),
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
'dateRangeOptions' => [
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime('-91 days')),
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last90Days'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime(date('Y') . '-01-01')),
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.thisYear'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime((date('Y') - 1) . '-01-01')),
|
||
'dateEnd' => date('Y-m-d', strtotime((date('Y') - 1) . '-12-31')),
|
||
'label' => __('stats.dateRange.lastYear'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime((date('Y') - 2) . '-01-01')),
|
||
'dateEnd' => date('Y-m-d', strtotime((date('Y') - 1) . '-12-31')),
|
||
'label' => __('stats.dateRange.lastTwoYears'),
|
||
],
|
||
],
|
||
'percentageStats' => $percentageStats,
|
||
'tableColumns' => [
|
||
[
|
||
'name' => 'name',
|
||
'label' => __('common.name'),
|
||
'value' => 'name',
|
||
],
|
||
[
|
||
'name' => 'dateRange',
|
||
'label' => $dateStart . ' — ' . $dateEnd,
|
||
'value' => 'dateRange',
|
||
],
|
||
[
|
||
'name' => 'total',
|
||
'label' => __('stats.total'),
|
||
'value' => 'total',
|
||
],
|
||
],
|
||
'tableRows' => $tableRows,
|
||
]
|
||
);
|
||
|
||
$templateMgr->setLocaleKeys([
|
||
'stats.descriptionForStat',
|
||
'stats.countWithYearlyAverage',
|
||
]);
|
||
$templateMgr->setState($statsComponent->getConfig());
|
||
$templateMgr->assign([
|
||
'pageComponent' => 'StatsEditorialPage',
|
||
'pageTitle' => __('stats.editorialActivity'),
|
||
]);
|
||
|
||
$templateMgr->display('stats/editorial.tpl');
|
||
}
|
||
|
||
/**
|
||
* Display published submissions statistics page
|
||
*
|
||
* @param PKPRequest $request
|
||
* @param array $args
|
||
*/
|
||
public function publications($args, $request)
|
||
{
|
||
$dispatcher = $request->getDispatcher();
|
||
$context = $request->getContext();
|
||
|
||
if (!$context) {
|
||
$dispatcher->handle404();
|
||
}
|
||
|
||
$templateMgr = TemplateManager::getManager($request);
|
||
$this->setupTemplate($request);
|
||
|
||
$dateStart = date('Y-m-d', strtotime('-31 days'));
|
||
$dateEnd = date('Y-m-d', strtotime('yesterday'));
|
||
$count = 30;
|
||
|
||
$timeline = Services::get('publicationStats')->getTimeline(PKPStatisticsHelper::STATISTICS_DIMENSION_DAY, [
|
||
'assocTypes' => [Application::ASSOC_TYPE_SUBMISSION],
|
||
'contextIds' => [$context->getId()],
|
||
'count' => $count,
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
]);
|
||
|
||
$statsComponent = new \PKP\components\PKPStatsPublicationPage(
|
||
$dispatcher->url($request, PKPApplication::ROUTE_API, $context->getPath(), 'stats/publications'),
|
||
[
|
||
'timeline' => $timeline,
|
||
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_DAY,
|
||
'timelineType' => 'abstract',
|
||
'tableColumns' => [
|
||
[
|
||
'name' => 'title',
|
||
'label' => __('common.title'),
|
||
],
|
||
[
|
||
'name' => 'abstractViews',
|
||
'label' => __('submission.abstractViews'),
|
||
'value' => 'abstractViews',
|
||
],
|
||
[
|
||
'name' => 'galleyViews',
|
||
'label' => __('stats.fileViews'),
|
||
'value' => 'galleyViews',
|
||
],
|
||
[
|
||
'name' => 'pdf',
|
||
'label' => __('stats.pdf'),
|
||
'value' => 'pdfViews',
|
||
],
|
||
[
|
||
'name' => 'html',
|
||
'label' => __('stats.html'),
|
||
'value' => 'htmlViews',
|
||
],
|
||
[
|
||
'name' => 'other',
|
||
'label' => __('common.other'),
|
||
'value' => 'otherViews',
|
||
],
|
||
[
|
||
'name' => 'total',
|
||
'label' => __('stats.total'),
|
||
'value' => 'total',
|
||
'orderBy' => 'total',
|
||
'initialOrderDirection' => true,
|
||
],
|
||
],
|
||
'count' => $count,
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
'dateRangeOptions' => [
|
||
[
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last30Days'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime('-91 days')),
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last90Days'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime('-12 months')),
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last12Months'),
|
||
],
|
||
[
|
||
'dateStart' => '',
|
||
'dateEnd' => '',
|
||
'label' => __('stats.dateRange.allDates'),
|
||
],
|
||
],
|
||
'orderBy' => 'total',
|
||
'orderDirection' => true,
|
||
]
|
||
);
|
||
|
||
$geoAPIEndPoint = null;
|
||
$geoStatsSetting = $context->getEnableGeoUsageStats($request->getSite());
|
||
switch ($geoStatsSetting) {
|
||
case PKPStatisticsHelper::STATISTICS_SETTING_COUNTRY:
|
||
$geoAPIEndPoint = 'countries';
|
||
break;
|
||
case PKPStatisticsHelper::STATISTICS_SETTING_REGION:
|
||
$geoAPIEndPoint = 'regions';
|
||
break;
|
||
case PKPStatisticsHelper::STATISTICS_SETTING_CITY:
|
||
$geoAPIEndPoint = 'cities';
|
||
break;
|
||
}
|
||
$templateMgr->setState($statsComponent->getConfig());
|
||
$templateMgr->assign([
|
||
'pageComponent' => 'StatsPublicationsPage',
|
||
'pageTitle' => __('stats.publicationStats'),
|
||
'pageWidth' => TemplateManager::PAGE_WIDTH_WIDE,
|
||
'geoReportType' => $geoAPIEndPoint
|
||
]);
|
||
|
||
$templateMgr->display('stats/publications.tpl');
|
||
}
|
||
|
||
/**
|
||
* Display published submissions statistics page
|
||
*
|
||
* @param PKPRequest $request
|
||
* @param array $args
|
||
*/
|
||
public function context($args, $request)
|
||
{
|
||
$dispatcher = $request->getDispatcher();
|
||
$context = $request->getContext();
|
||
|
||
if (!$context) {
|
||
$dispatcher->handle404();
|
||
}
|
||
|
||
$templateMgr = TemplateManager::getManager($request);
|
||
$this->setupTemplate($request);
|
||
|
||
$dateStart = date('Y-m-d', strtotime('-31 days'));
|
||
$dateEnd = date('Y-m-d', strtotime('yesterday'));
|
||
|
||
$timeline = Services::get('contextStats')->getTimeline(PKPStatisticsHelper::STATISTICS_DIMENSION_DAY, [
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
'contextIds' => [$context->getId()]
|
||
]);
|
||
|
||
$statsComponent = new \PKP\components\PKPStatsContextPage(
|
||
$dispatcher->url($request, PKPApplication::ROUTE_API, $context->getPath(), 'stats/contexts/' . $context->getId()),
|
||
[
|
||
'timeline' => $timeline,
|
||
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_DAY,
|
||
'tableColumns' => [
|
||
[
|
||
'name' => 'title',
|
||
'label' => __('common.title'),
|
||
],
|
||
[
|
||
'name' => 'total',
|
||
'label' => __('stats.total'),
|
||
'value' => 'total',
|
||
],
|
||
],
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
'dateRangeOptions' => [
|
||
[
|
||
'dateStart' => $dateStart,
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last30Days'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime('-91 days')),
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last90Days'),
|
||
],
|
||
[
|
||
'dateStart' => date('Y-m-d', strtotime('-12 months')),
|
||
'dateEnd' => $dateEnd,
|
||
'label' => __('stats.dateRange.last12Months'),
|
||
],
|
||
[
|
||
'dateStart' => '',
|
||
'dateEnd' => '',
|
||
'label' => __('stats.dateRange.allDates'),
|
||
],
|
||
]
|
||
]
|
||
);
|
||
|
||
$templateMgr->setState($statsComponent->getConfig());
|
||
$templateMgr->assign([
|
||
'pageComponent' => 'StatsContextPage',
|
||
'pageTitle' => __('stats.contextStats'),
|
||
'pageWidth' => TemplateManager::PAGE_WIDTH_WIDE,
|
||
]);
|
||
|
||
$templateMgr->display('stats/context.tpl');
|
||
}
|
||
|
||
/**
|
||
* Display list of available COUNTER R5 reports
|
||
*/
|
||
public function counterR5(array $args, Request $request): void
|
||
{
|
||
$templateMgr = TemplateManager::getManager($request);
|
||
$this->setupTemplate($request);
|
||
|
||
$apiUrl = $request->getDispatcher()->url($request, PKPApplication::ROUTE_API, $request->getContext()->getPath(), 'stats/sushi');
|
||
|
||
$context = $request->getContext();
|
||
$locales = $context->getSupportedFormLocaleNames();
|
||
$locales = array_map(fn (string $locale, string $name) => ['key' => $locale, 'label' => $name], array_keys($locales), $locales);
|
||
|
||
$counterReportForm = new \APP\components\forms\counter\CounterReportForm($apiUrl, $locales);
|
||
|
||
$counterReportsListPanel = new \PKP\components\listPanels\PKPCounterReportsListPanel(
|
||
'counterReportsListPanel',
|
||
__('manager.statistics.counterR5Reports'),
|
||
[
|
||
'apiUrl' => $apiUrl,
|
||
'form' => $counterReportForm,
|
||
]
|
||
);
|
||
|
||
$earliestDate = CounterR5Report::getEarliestDate();
|
||
$lastDate = CounterR5Report::getLastDate();
|
||
|
||
$templateMgr->setState([
|
||
'components' => [
|
||
$counterReportsListPanel->id => $counterReportsListPanel->getConfig(),
|
||
],
|
||
]);
|
||
$templateMgr->assign([
|
||
'pageComponent' => 'CounterReportsPage',
|
||
'usagePossible' => $lastDate > $earliestDate,
|
||
]);
|
||
$templateMgr->display('stats/counterReports.tpl');
|
||
}
|
||
|
||
/**
|
||
* Display users stats
|
||
*
|
||
*/
|
||
public function users(array $args, Request $request): void
|
||
{
|
||
$dispatcher = $request->getDispatcher();
|
||
$context = $request->getContext();
|
||
|
||
if (!$context) {
|
||
$dispatcher->handle404();
|
||
}
|
||
|
||
// The POST handler is here merely to serve a redirection URL to the Vue component
|
||
if ($request->isPost()) {
|
||
echo $dispatcher->url($request, PKPApplication::ROUTE_API, $context->getPath(), 'users/report', null, null, $request->getUserVars());
|
||
exit;
|
||
}
|
||
|
||
$templateMgr = TemplateManager::getManager($request);
|
||
$this->setupTemplate($request);
|
||
|
||
$context = $request->getContext();
|
||
$selfUrl = $dispatcher->url($request, PKPApplication::ROUTE_PAGE, $context->getPath(), 'stats', 'users');
|
||
$reportForm = new \PKP\components\forms\statistics\users\ReportForm($selfUrl, $context);
|
||
|
||
$templateMgr->setState([
|
||
'components' => [
|
||
'usersReportForm' => $reportForm->getConfig()
|
||
]
|
||
]);
|
||
$templateMgr->assign([
|
||
'pageTitle' => __('stats.userStatistics'),
|
||
'pageComponent' => 'StatsUsersPage',
|
||
'userStats' => array_map(
|
||
function ($item) {
|
||
$item['name'] = __($item['name']);
|
||
return $item;
|
||
},
|
||
Repo::user()->getRolesOverview(Repo::user()->getCollector()->filterByContextIds(['contextId' => $context->getId()]))
|
||
),
|
||
]);
|
||
$templateMgr->display('stats/users.tpl');
|
||
}
|
||
|
||
/**
|
||
* Route to other Reports operations
|
||
*
|
||
* @param array $args
|
||
* @param PKPRequest $request
|
||
*/
|
||
public function reports($args, $request)
|
||
{
|
||
$path = array_shift($args);
|
||
switch ($path) {
|
||
case '':
|
||
case 'reports':
|
||
$this->displayReports($args, $request);
|
||
break;
|
||
case 'report':
|
||
$this->report($args, $request);
|
||
break;
|
||
default: assert(false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display report possibilities (report plugins)
|
||
*
|
||
* @param array $args
|
||
* @param PKPRequest $request
|
||
*/
|
||
public function displayReports($args, $request)
|
||
{
|
||
$dispatcher = $request->getDispatcher();
|
||
$context = $request->getContext();
|
||
|
||
if (!$context) {
|
||
$dispatcher->handle404();
|
||
}
|
||
|
||
$templateMgr = TemplateManager::getManager($request);
|
||
$this->setupTemplate($request);
|
||
|
||
$reportPlugins = PluginRegistry::loadCategory('reports');
|
||
$templateMgr->assign('reportPlugins', $reportPlugins);
|
||
|
||
$templateMgr->assign([
|
||
'pageTitle' => __('manager.statistics.reports'),
|
||
]);
|
||
$templateMgr->display('stats/reports.tpl');
|
||
}
|
||
|
||
/**
|
||
* Delegates to plugins operations
|
||
* related to report generation.
|
||
*
|
||
* @param array $args
|
||
* @param Request $request
|
||
*/
|
||
public function report($args, $request)
|
||
{
|
||
$this->setupTemplate($request);
|
||
|
||
$pluginName = $request->getUserVar('pluginName');
|
||
$reportPlugins = PluginRegistry::loadCategory('reports');
|
||
|
||
if ($pluginName == '' || !isset($reportPlugins[$pluginName])) {
|
||
$request->redirect(null, null, 'stats', 'reports');
|
||
}
|
||
|
||
$plugin = $reportPlugins[$pluginName];
|
||
$plugin->display($args, $request);
|
||
}
|
||
|
||
//
|
||
// Protected methods.
|
||
//
|
||
/**
|
||
* Get a description for stats that require one
|
||
*
|
||
* @param string $key
|
||
*/
|
||
protected function _getStatDescription($key)
|
||
{
|
||
switch ($key) {
|
||
case 'daysToDecision': return __('stats.description.daysToDecision');
|
||
case 'acceptanceRate': return __('stats.description.acceptRejectRate');
|
||
case 'declineRate': return __('stats.description.acceptRejectRate');
|
||
case 'submissionsSkipped': return __('stats.description.submissionsSkipped');
|
||
}
|
||
return '';
|
||
}
|
||
}
|