first commit
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file api/v1/stats/contexts/PKPStatsContextHandler.php
|
||||
*
|
||||
* Copyright (c) 2022 Simon Fraser University
|
||||
* Copyright (c) 2022 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class PKPStatsContextHandler
|
||||
*
|
||||
* @ingroup api_v1_stats
|
||||
*
|
||||
* @brief Handle API requests for context statistics.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace PKP\API\v1\stats\contexts;
|
||||
|
||||
use APP\core\Services;
|
||||
use PKP\core\APIResponse;
|
||||
use PKP\handler\APIHandler;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\security\authorization\PolicySet;
|
||||
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
|
||||
use PKP\security\authorization\UserRolesRequiredPolicy;
|
||||
use PKP\security\Role;
|
||||
use PKP\statistics\PKPStatisticsHelper;
|
||||
use Slim\Http\Request as SlimHttpRequest;
|
||||
|
||||
class PKPStatsContextHandler extends APIHandler
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->_handlerPath = 'stats/contexts';
|
||||
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
|
||||
$this->_endpoints = [
|
||||
'GET' => [
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern(),
|
||||
'handler' => [$this, 'getMany'],
|
||||
'roles' => [Role::ROLE_ID_SITE_ADMIN]
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/timeline',
|
||||
'handler' => [$this, 'getManyTimeline'],
|
||||
'roles' => [Role::ROLE_ID_SITE_ADMIN]
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
|
||||
'handler' => [$this, 'get'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/timeline',
|
||||
'handler' => [$this, 'getTimeline'],
|
||||
'roles' => $roles
|
||||
],
|
||||
],
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPHandler::authorize()
|
||||
*/
|
||||
public function authorize($request, &$args, $roleAssignments)
|
||||
{
|
||||
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
|
||||
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
|
||||
foreach ($roleAssignments as $role => $operations) {
|
||||
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
|
||||
}
|
||||
$this->addPolicy($rolePolicy);
|
||||
return parent::authorize($request, $args, $roleAssignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total views of the homepages for a set of contexts
|
||||
*/
|
||||
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'count' => 30,
|
||||
'offset' => 0,
|
||||
'orderDirection' => PKPStatisticsHelper::STATISTICS_ORDER_DESC,
|
||||
];
|
||||
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
'count',
|
||||
'offset',
|
||||
'orderDirection',
|
||||
'searchPhrase',
|
||||
'contextIds',
|
||||
]);
|
||||
|
||||
Hook::call('API::stats::contexts::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
if (!in_array($allowedParams['orderDirection'], [PKPStatisticsHelper::STATISTICS_ORDER_ASC, PKPStatisticsHelper::STATISTICS_ORDER_DESC])) {
|
||||
return $response->withStatus(400)->withJsonError('api.stats.400.invalidOrderDirection');
|
||||
}
|
||||
|
||||
// Identify contexts which should be included in the results when a searchPhrase is passed
|
||||
if (!empty($allowedParams['searchPhrase'])) {
|
||||
$allowedContextIds = empty($allowedParams['contextIds']) ? [] : $allowedParams['contextIds'];
|
||||
$allowedParams['contextIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedContextIds);
|
||||
|
||||
if (empty($allowedParams['contextIds'])) {
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getContextReportColumnNames();
|
||||
return $response->withCSV([], $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => [],
|
||||
'itemsMax' => 0,
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of contexts with their total views matching the params
|
||||
$statsService = Services::get('contextStats');
|
||||
$totalMetrics = $statsService->getTotals($allowedParams);
|
||||
|
||||
// Get the stats for each context
|
||||
$items = [];
|
||||
foreach ($totalMetrics as $totalMetric) {
|
||||
$contextId = $totalMetric->context_id;
|
||||
$contextViews = $totalMetric->metric;
|
||||
|
||||
if ($responseCSV) {
|
||||
$items[] = $this->getItemForCSV($contextId, $contextViews);
|
||||
} else {
|
||||
$items[] = $this->getItemForJSON($slimRequest, $contextId, $contextViews);
|
||||
}
|
||||
}
|
||||
|
||||
$itemsMax = $statsService->getCount($allowedParams);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getContextReportColumnNames();
|
||||
return $response->withCSV($items, $csvColumnNames, $itemsMax);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => $items,
|
||||
'itemsMax' => $itemsMax,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a monthly or daily timeline of total views for a set of contexts
|
||||
*/
|
||||
public function getManyTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH,
|
||||
];
|
||||
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
'timelineInterval',
|
||||
'searchPhrase',
|
||||
'contextIds',
|
||||
]);
|
||||
|
||||
Hook::call('API::stats::contexts::timeline::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
if (!$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
|
||||
return $response->withStatus(400)->withJsonError('api.stats.400.wrongTimelineInterval');
|
||||
}
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
// Identify contexts which should be included in the results when a searchPhrase is passed
|
||||
if (!empty($allowedParams['searchPhrase'])) {
|
||||
$allowedContextIds = empty($allowedParams['contextIds']) ? [] : $allowedParams['contextIds'];
|
||||
$allowedParams['contextIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedContextIds);
|
||||
|
||||
if (empty($allowedParams['contextIds'])) {
|
||||
$dateStart = empty($allowedParams['dateStart']) ? PKPStatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart'];
|
||||
$dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd'];
|
||||
$emptyTimeline = Services::get('contextStats')->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
|
||||
return $response->withCSV($emptyTimeline, $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson($emptyTimeline, 200);
|
||||
}
|
||||
}
|
||||
|
||||
$data = Services::get('contextStats')->getTimeline($allowedParams['timelineInterval'], $allowedParams);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
|
||||
return $response->withCSV($data, $csvColumnNames, count($data));
|
||||
}
|
||||
return $response->withJson($data, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single context's usage statistics
|
||||
*/
|
||||
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$request = $this->getRequest();
|
||||
|
||||
$context = Services::get('context')->get((int) $args['contextId']);
|
||||
if (!$context) {
|
||||
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
|
||||
}
|
||||
// Don't allow to get one context from a different context's endpoint
|
||||
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
|
||||
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
|
||||
}
|
||||
|
||||
$allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
]);
|
||||
|
||||
Hook::call('API::stats::context::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
|
||||
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
|
||||
|
||||
$statsService = Services::get('contextStats');
|
||||
$contextViews = $statsService->getTotal($context->getId(), $dateStart, $dateEnd);
|
||||
|
||||
// Get basic context details for display
|
||||
$propertyArgs = [
|
||||
'request' => $request,
|
||||
'slimRequest' => $slimRequest,
|
||||
];
|
||||
$contextProps = Services::get('context')->getSummaryProperties($context, $propertyArgs);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getContextReportColumnNames();
|
||||
$items = [$this->getItemForCSV($context->getId(), $contextViews)];
|
||||
return $response->withCSV($items, $csvColumnNames, 1);
|
||||
}
|
||||
return $response->withJson([
|
||||
'total' => $contextViews,
|
||||
'context' => $contextProps
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a monthly or daily timeline of total views for a context
|
||||
*/
|
||||
public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$request = $this->getRequest();
|
||||
|
||||
$context = Services::get('context')->get((int) $args['contextId']);
|
||||
if (!$context) {
|
||||
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
|
||||
}
|
||||
// Don't allow to get one context from a different context's endpoint
|
||||
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
|
||||
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
|
||||
}
|
||||
|
||||
$defaultParams = [
|
||||
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH,
|
||||
];
|
||||
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
'timelineInterval',
|
||||
]);
|
||||
|
||||
$allowedParams['contextIds'] = [$context->getId()];
|
||||
|
||||
Hook::call('API::stats::context::timeline::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
if (!$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
|
||||
return $response->withStatus(400)->withJsonError('api.stats.400.wrongTimelineInterval');
|
||||
}
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
$statsService = Services::get('contextStats');
|
||||
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
|
||||
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
|
||||
return $response->withCSV($data, $csvColumnNames, count($data));
|
||||
}
|
||||
return $response->withJson($data, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to filter and sanitize the request params
|
||||
*
|
||||
* Only allows the specified params through and enforces variable
|
||||
* type where needed.
|
||||
*/
|
||||
protected function _processAllowedParams(array $requestParams, array $allowedParams): array
|
||||
{
|
||||
$returnParams = [];
|
||||
foreach ($requestParams as $requestParam => $value) {
|
||||
if (!in_array($requestParam, $allowedParams)) {
|
||||
continue;
|
||||
}
|
||||
switch ($requestParam) {
|
||||
case 'dateStart':
|
||||
case 'dateEnd':
|
||||
case 'timelineInterval':
|
||||
$returnParams[$requestParam] = $value;
|
||||
break;
|
||||
|
||||
case 'count':
|
||||
$returnParams[$requestParam] = min(100, (int) $value);
|
||||
break;
|
||||
|
||||
case 'offset':
|
||||
$returnParams[$requestParam] = (int) $value;
|
||||
break;
|
||||
|
||||
case 'orderDirection':
|
||||
$returnParams[$requestParam] = strtoupper($value);
|
||||
break;
|
||||
case 'contextIds':
|
||||
if (is_string($value) && strpos($value, ',') > -1) {
|
||||
$value = explode(',', $value);
|
||||
} elseif (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
$returnParams[$requestParam] = array_map('intval', $value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $returnParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to get the contextIds param when a searchPhase
|
||||
* param is also passed.
|
||||
*
|
||||
* If the searchPhrase and contextIds params were both passed in the
|
||||
* request, then we only return IDs that match both conditions.
|
||||
*/
|
||||
protected function _processSearchPhrase(string $searchPhrase, array $contextIds = []): array
|
||||
{
|
||||
$searchPhraseContextIds = Services::get('context')->getIds(['searchPhrase' => $searchPhrase]);
|
||||
if (!empty($contextIds)) {
|
||||
return array_intersect($contextIds, $searchPhraseContextIds->toArray());
|
||||
}
|
||||
return $searchPhraseContextIds->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSV report columns
|
||||
*/
|
||||
protected function _getContextReportColumnNames(): array
|
||||
{
|
||||
return [
|
||||
__('common.id'),
|
||||
__('common.title'),
|
||||
__('stats.total'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSV row with context index page metrics
|
||||
*/
|
||||
protected function getItemForCSV(int $contextId, int $contextViews): array
|
||||
{
|
||||
// Get context title for display
|
||||
$contexts = Services::get('context')->getManySummary([]);
|
||||
$context = array_filter($contexts, function ($context) use ($contextId) {
|
||||
return $context->id == $contextId;
|
||||
});
|
||||
$title = current($context)->name;
|
||||
return [
|
||||
$contextId,
|
||||
$title,
|
||||
$contextViews
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON data with context index page metrics
|
||||
*/
|
||||
protected function getItemForJSON(SlimHttpRequest $slimRequest, int $contextId, int $contextViews): array
|
||||
{
|
||||
// Get basic context details for display
|
||||
$propertyArgs = [
|
||||
'request' => $this->getRequest(),
|
||||
'slimRequest' => $slimRequest,
|
||||
];
|
||||
$context = Services::get('context')->get($contextId);
|
||||
$contextProps = Services::get('context')->getSummaryProperties($context, $propertyArgs);
|
||||
return [
|
||||
'total' => $contextViews,
|
||||
'context' => $contextProps,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the timeline interval is valid
|
||||
*/
|
||||
protected function isValidTimelineInterval(string $interval): bool
|
||||
{
|
||||
return in_array($interval, [
|
||||
PKPStatisticsHelper::STATISTICS_DIMENSION_DAY,
|
||||
PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file api/v1/stats/editorial/PKPStatsEditorialHandler.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 PKPStatsEditorialHandler
|
||||
*
|
||||
* @ingroup api_v1_stats
|
||||
*
|
||||
* @brief Handle API requests for publication statistics.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace PKP\API\v1\stats\editorial;
|
||||
|
||||
use APP\core\Services;
|
||||
use PKP\core\APIResponse;
|
||||
use PKP\handler\APIHandler;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\security\authorization\ContextAccessPolicy;
|
||||
use PKP\security\authorization\PolicySet;
|
||||
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
|
||||
use PKP\security\authorization\UserRolesRequiredPolicy;
|
||||
use PKP\security\Role;
|
||||
use Slim\Http\Request;
|
||||
|
||||
abstract class PKPStatsEditorialHandler extends APIHandler
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->_handlerPath = 'stats/editorial';
|
||||
$this->_endpoints = [
|
||||
'GET' => [
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern(),
|
||||
'handler' => [$this, 'get'],
|
||||
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/averages',
|
||||
'handler' => [$this, 'getAverages'],
|
||||
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
|
||||
],
|
||||
],
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/** The name of the section ids query param for this application */
|
||||
abstract public function getSectionIdsQueryParam();
|
||||
|
||||
/**
|
||||
* @copydoc PKPHandler::authorize()
|
||||
*/
|
||||
public function authorize($request, &$args, $roleAssignments)
|
||||
{
|
||||
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
|
||||
|
||||
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
|
||||
|
||||
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
|
||||
foreach ($roleAssignments as $role => $operations) {
|
||||
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
|
||||
}
|
||||
$this->addPolicy($rolePolicy);
|
||||
|
||||
return parent::authorize($request, $args, $roleAssignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get editorial stats
|
||||
*
|
||||
* Returns information on submissions received, accepted, declined,
|
||||
* average response times and more.
|
||||
*
|
||||
* @param Request $slimRequest Slim request object
|
||||
* @param APIResponse $response Response
|
||||
* @param array $args
|
||||
*
|
||||
* @return APIResponse Response
|
||||
*/
|
||||
public function get($slimRequest, $response, $args)
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
if (!$request->getContext()) {
|
||||
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
|
||||
}
|
||||
|
||||
$params = [];
|
||||
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
|
||||
foreach ($slimRequest->getQueryParams() as $param => $value) {
|
||||
switch ($param) {
|
||||
case 'dateStart':
|
||||
case 'dateEnd':
|
||||
$params[$param] = $value;
|
||||
break;
|
||||
|
||||
case $sectionIdsQueryParam:
|
||||
if (is_string($value) && str_contains($value, ',')) {
|
||||
$value = explode(',', $value);
|
||||
} elseif (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
$params[$param] = array_map('intval', $value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Hook::call('API::stats::editorial::params', [&$params, $slimRequest]);
|
||||
|
||||
$params['contextIds'] = [$request->getContext()->getId()];
|
||||
|
||||
$result = $this->_validateStatDates($params);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
return $response->withJson(array_map(
|
||||
function ($item) {
|
||||
$item['name'] = __($item['name']);
|
||||
return $item;
|
||||
},
|
||||
Services::get('editorialStats')->getOverview($params)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yearly averages of editorial stats
|
||||
*
|
||||
* Returns information on average submissions received, accepted
|
||||
* and declined per year.
|
||||
*
|
||||
* @param Request $slimRequest Slim request object
|
||||
* @param APIResponse $response Response
|
||||
* @param array $args
|
||||
*
|
||||
* @return APIResponse Response
|
||||
*/
|
||||
public function getAverages($slimRequest, $response, $args)
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
if (!$request->getContext()) {
|
||||
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
|
||||
}
|
||||
|
||||
$params = [];
|
||||
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
|
||||
foreach ($slimRequest->getQueryParams() as $param => $value) {
|
||||
switch ($param) {
|
||||
case $sectionIdsQueryParam:
|
||||
if (is_string($value) && str_contains($value, ',')) {
|
||||
$value = explode(',', $value);
|
||||
} elseif (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
$params[$param] = array_map('intval', $value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Hook::call('API::stats::editorial::averages::params', [&$params, $slimRequest]);
|
||||
|
||||
$params['contextIds'] = [$request->getContext()->getId()];
|
||||
|
||||
return $response->withJson(Services::get('editorialStats')->getAverages($params));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,980 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file api/v1/stats/publications/PKPStatsPublicationHandler.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 PKPStatsPublicationHandler
|
||||
*
|
||||
* @ingroup api_v1_stats
|
||||
*
|
||||
* @brief Handle API requests for submission statistics.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace PKP\API\v1\stats\publications;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\core\Services;
|
||||
use APP\facades\Repo;
|
||||
use APP\statistics\StatisticsHelper;
|
||||
use APP\submission\Submission;
|
||||
use PKP\core\APIResponse;
|
||||
use PKP\handler\APIHandler;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\security\authorization\ContextAccessPolicy;
|
||||
use PKP\security\authorization\PolicySet;
|
||||
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
|
||||
use PKP\security\authorization\SubmissionAccessPolicy;
|
||||
use PKP\security\authorization\UserRolesRequiredPolicy;
|
||||
use PKP\security\Role;
|
||||
use Slim\Http\Request as SlimHttpRequest;
|
||||
use Sokil\IsoCodes\IsoCodesFactory;
|
||||
|
||||
abstract class PKPStatsPublicationHandler extends APIHandler
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->_handlerPath = 'stats/publications';
|
||||
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR];
|
||||
$this->_endpoints = [
|
||||
'GET' => [
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern(),
|
||||
'handler' => [$this, 'getMany'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/timeline',
|
||||
'handler' => [$this, 'getManyTimeline'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}',
|
||||
'handler' => [$this, 'get'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/timeline',
|
||||
'handler' => [$this, 'getTimeline'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/files',
|
||||
'handler' => [$this, 'getManyFiles'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/countries',
|
||||
'handler' => [$this, 'getManyCountries'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/regions',
|
||||
'handler' => [$this, 'getManyRegions'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/cities',
|
||||
'handler' => [$this, 'getManyCities'],
|
||||
'roles' => $roles
|
||||
],
|
||||
],
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/** The name of the section ids query param for this application */
|
||||
abstract public function getSectionIdsQueryParam();
|
||||
|
||||
/**
|
||||
* @copydoc PKPHandler::authorize()
|
||||
*/
|
||||
public function authorize($request, &$args, $roleAssignments)
|
||||
{
|
||||
$routeName = null;
|
||||
$slimRequest = $this->getSlimRequest();
|
||||
|
||||
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
|
||||
|
||||
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
|
||||
|
||||
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
|
||||
foreach ($roleAssignments as $role => $operations) {
|
||||
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
|
||||
}
|
||||
$this->addPolicy($rolePolicy);
|
||||
|
||||
if (!is_null($slimRequest) && ($route = $slimRequest->getAttribute('route'))) {
|
||||
$routeName = $route->getName();
|
||||
}
|
||||
if (in_array($routeName, ['get', 'getTimeline'])) {
|
||||
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
|
||||
}
|
||||
|
||||
return parent::authorize($request, $args, $roleAssignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to filter and sanitize the application specific request params
|
||||
*/
|
||||
protected function _processAppSpecificAllowedParams(string $requestParam, mixed $value, array &$returnParams): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed parameters for getMany methods:
|
||||
* getMany(), getManyFiles(), getManyCountries(), getManyRegions(), getManyCities
|
||||
*/
|
||||
protected function getManyAllowedParams()
|
||||
{
|
||||
$allowedParams = [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
'count',
|
||||
'offset',
|
||||
'orderDirection',
|
||||
'searchPhrase',
|
||||
'submissionIds',
|
||||
];
|
||||
$allowedParams[] = $this->getSectionIdsQueryParam();
|
||||
return $allowedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed parameters for getManyTimeline method
|
||||
*/
|
||||
protected function getManyTimelineAllowedParams()
|
||||
{
|
||||
$allowedParams = [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
'timelineInterval',
|
||||
'searchPhrase',
|
||||
'submissionIds',
|
||||
'type'
|
||||
];
|
||||
$allowedParams[] = $this->getSectionIdsQueryParam();
|
||||
return $allowedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage stats for a set of publications
|
||||
*
|
||||
* Returns total views by abstract, all galleys, pdf galleys,
|
||||
* html galleys, and other galleys.
|
||||
*/
|
||||
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
|
||||
];
|
||||
$initAllowedParams = $this->getManyAllowedParams();
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
|
||||
|
||||
Hook::call('API::stats::publications::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
// Check/validate, filter and sanitize the request params
|
||||
try {
|
||||
$allowedParams = $this->validateParams($allowedParams);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 200) {
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getSubmissionReportColumnNames();
|
||||
return $response->withCSV([], $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => [],
|
||||
'itemsMax' => 0,
|
||||
], 200);
|
||||
}
|
||||
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
|
||||
}
|
||||
|
||||
$statsService = Services::get('publicationStats');
|
||||
// Get a list of top submissions by total views
|
||||
$totalMetrics = $statsService->getTotals($allowedParams);
|
||||
|
||||
// Get the stats for each submission
|
||||
$items = [];
|
||||
foreach ($totalMetrics as $totalMetric) {
|
||||
$submissionId = $totalMetric->submission_id;
|
||||
|
||||
// get abstract, pdf, html and other views for the submission
|
||||
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
|
||||
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
|
||||
$metricsByType = $statsService->getTotalsByType($submissionId, $this->getRequest()->getContext()->getId(), $dateStart, $dateEnd);
|
||||
|
||||
if ($responseCSV) {
|
||||
$items[] = $this->getItemForCSV($submissionId, $metricsByType['abstract'], $metricsByType['pdf'], $metricsByType['html'], $metricsByType['other']);
|
||||
} else {
|
||||
$items[] = $this->getItemForJSON($submissionId, $metricsByType['abstract'], $metricsByType['pdf'], $metricsByType['html'], $metricsByType['other']);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the total count of submissions
|
||||
$itemsMax = $statsService->getCount($allowedParams);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getSubmissionReportColumnNames();
|
||||
return $response->withCSV($items, $csvColumnNames, $itemsMax);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => $items,
|
||||
'itemsMax' => $itemsMax,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total abstract or files views for a set of submissions
|
||||
* in a timeline broken down by month or day
|
||||
*/
|
||||
public function getManyTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH,
|
||||
];
|
||||
$initAllowedParams = $this->getManyTimelineAllowedParams();
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
|
||||
|
||||
Hook::call('API::stats::publications::timeline::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
$statsService = Services::get('publicationStats');
|
||||
// Check/validate, filter and sanitize the request params
|
||||
try {
|
||||
$allowedParams = $this->validateParams($allowedParams);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 200) {
|
||||
$dateStart = empty($allowedParams['dateStart']) ? StatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart'];
|
||||
$dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd'];
|
||||
$emptyTimeline = $statsService->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $statsService->getTimelineReportColumnNames();
|
||||
return $response->withCSV($emptyTimeline, $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson($emptyTimeline, 200);
|
||||
}
|
||||
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
|
||||
}
|
||||
|
||||
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION];
|
||||
if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') {
|
||||
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION_FILE];
|
||||
};
|
||||
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $statsService->getTimelineReportColumnNames();
|
||||
return $response->withCSV($data, $csvColumnNames, count($data));
|
||||
}
|
||||
return $response->withJson($data, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single submission's usage statistics
|
||||
*/
|
||||
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
|
||||
|
||||
$allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
]);
|
||||
|
||||
Hook::call('API::stats::publication::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
$statsService = Services::get('publicationStats');
|
||||
// get abstract, pdf, html and other views for the submission
|
||||
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
|
||||
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
|
||||
$metricsByType = $statsService->getTotalsByType($submission->getId(), $request->getContext()->getId(), $dateStart, $dateEnd);
|
||||
|
||||
$galleyViews = $metricsByType['pdf'] + $metricsByType['html'] + $metricsByType['other'];
|
||||
return $response->withJson([
|
||||
'abstractViews' => $metricsByType['abstract'],
|
||||
'galleyViews' => $galleyViews,
|
||||
'pdfViews' => $metricsByType['pdf'],
|
||||
'htmlViews' => $metricsByType['html'],
|
||||
'otherViews' => $metricsByType['other'],
|
||||
'publication' => Repo::submission()->getSchemaMap()->mapToStats($submission),
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total abstract of files views for a submission broken down by
|
||||
* month or day
|
||||
*/
|
||||
public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
|
||||
|
||||
$defaultParams = [
|
||||
'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH,
|
||||
];
|
||||
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, [
|
||||
'dateStart',
|
||||
'dateEnd',
|
||||
'timelineInterval',
|
||||
'type'
|
||||
]);
|
||||
|
||||
Hook::call('API::stats::publication::timeline::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
$allowedParams['contextIds'] = [$request->getContext()->getId()];
|
||||
$allowedParams['submissionIds'] = [$submission->getId()];
|
||||
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION];
|
||||
if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') {
|
||||
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION_FILE];
|
||||
};
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
if (!in_array($allowedParams['timelineInterval'], [StatisticsHelper::STATISTICS_DIMENSION_DAY, StatisticsHelper::STATISTICS_DIMENSION_MONTH])) {
|
||||
return $response->withStatus(400)->withJsonError('api.stats.400.invalidTimelineInterval');
|
||||
}
|
||||
|
||||
$statsService = Services::get('publicationStats');
|
||||
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
|
||||
|
||||
return $response->withJson($data, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total usage stats for a set of submission files.
|
||||
*/
|
||||
public function getManyFiles(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
|
||||
];
|
||||
$initAllowedParams = $this->getManyAllowedParams();
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
|
||||
|
||||
Hook::call('API::stats::publications::files::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
// Check/validate, filter and sanitize the request params
|
||||
try {
|
||||
$allowedParams = $this->validateParams($allowedParams);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 200) {
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getFileReportColumnNames();
|
||||
return $response->withCSV([], $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => [],
|
||||
'itemsMax' => 0,
|
||||
], 200);
|
||||
}
|
||||
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
|
||||
}
|
||||
|
||||
$statsService = Services::get('publicationStats');
|
||||
$filesMetrics = $statsService->getFilesTotals($allowedParams);
|
||||
|
||||
$items = $submissionTitles = [];
|
||||
foreach ($filesMetrics as $fileMetric) {
|
||||
$submissionId = $fileMetric->submission_id;
|
||||
$submissionFileId = $fileMetric->submission_file_id;
|
||||
$downloads = $fileMetric->metric;
|
||||
$type = $fileMetric->assoc_type;
|
||||
|
||||
if (!isset($submissionTitles[$submissionId])) {
|
||||
$submission = Repo::submission()->get($submissionId);
|
||||
$submissionTitles[$submissionId] = $submission->getCurrentPublication()->getLocalizedTitle();
|
||||
}
|
||||
|
||||
if ($responseCSV) {
|
||||
$items[] = $this->getFilesCSVItem($submissionFileId, $downloads, $type, $submissionId, $submissionTitles[$submissionId]);
|
||||
} else {
|
||||
$items[] = $this->getFilesJSONItem($submissionFileId, $downloads, $type, $submissionId, $submissionTitles[$submissionId]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the total count of submissions files
|
||||
$itemsMax = $statsService->getFilesCount($allowedParams);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getFileReportColumnNames();
|
||||
return $response->withCSV($items, $csvColumnNames, $itemsMax);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => $items,
|
||||
'itemsMax' => $itemsMax,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage stats for a set of countries
|
||||
*
|
||||
* Returns total count of views, downloads, unique views and unique downloads in a country
|
||||
*/
|
||||
public function getManyCountries(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
|
||||
];
|
||||
$initAllowedParams = $this->getManyAllowedParams();
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
|
||||
|
||||
Hook::call('API::stats::publications::countries::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
// Check/validate, filter and sanitize the request params
|
||||
try {
|
||||
$allowedParams = $this->validateParams($allowedParams);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 200) {
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
|
||||
return $response->withCSV([], $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => [],
|
||||
'itemsMax' => 0,
|
||||
], 200);
|
||||
}
|
||||
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
|
||||
}
|
||||
|
||||
$statsService = Services::get('geoStats');
|
||||
// Get a list of top countries by total views
|
||||
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
|
||||
|
||||
// Get the stats for each country
|
||||
$items = [];
|
||||
$isoCodes = app(IsoCodesFactory::class);
|
||||
foreach ($totals as $total) {
|
||||
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
|
||||
$countryName = $country ? $country->getLocalName() : $total->country;
|
||||
|
||||
$metric = $total->metric;
|
||||
$metric_unique = $total->metric_unique;
|
||||
if ($responseCSV) {
|
||||
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName);
|
||||
} else {
|
||||
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the total count of countries
|
||||
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
|
||||
return $response->withCSV($items, $csvColumnNames, $itemsMax);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => $items,
|
||||
'itemsMax' => $itemsMax,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage stats for set of regions
|
||||
*
|
||||
* Returns total count of views, downloads, unique views and unique downloads in a region
|
||||
*/
|
||||
public function getManyRegions(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
|
||||
];
|
||||
$initAllowedParams = $this->getManyAllowedParams();
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
|
||||
|
||||
Hook::call('API::stats::publications::regions::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
// Check/validate, filter and sanitize the request params
|
||||
try {
|
||||
$allowedParams = $this->validateParams($allowedParams);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 200) {
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_REGION);
|
||||
return $response->withCSV([], $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => [],
|
||||
'itemsMax' => 0,
|
||||
], 200);
|
||||
}
|
||||
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
|
||||
}
|
||||
|
||||
$statsService = Services::get('geoStats');
|
||||
// Get a list of top regions by total views
|
||||
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_REGION);
|
||||
|
||||
// Get the stats for each region
|
||||
$items = [];
|
||||
$isoCodes = app(IsoCodesFactory::class);
|
||||
foreach ($totals as $total) {
|
||||
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
|
||||
$countryName = $country ? $country->getLocalName() : __('stats.unknown');
|
||||
$regionName = !empty($total->region) ? $total->region : __('stats.unknown');
|
||||
if (!empty($total->country) && !empty($total->region)) {
|
||||
$regionCode = $total->country . '-' . $total->region;
|
||||
$region = $isoCodes->getSubdivisions()->getByCode($regionCode);
|
||||
$regionName = $region ? $region->getLocalName() : $regionCode;
|
||||
}
|
||||
|
||||
$metric = $total->metric;
|
||||
$metric_unique = $total->metric_unique;
|
||||
if ($responseCSV) {
|
||||
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName, $regionName);
|
||||
} else {
|
||||
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName, $regionName);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the total count of regions
|
||||
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_REGION);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_REGION);
|
||||
return $response->withCSV($items, $csvColumnNames, $itemsMax);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => $items,
|
||||
'itemsMax' => $itemsMax,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage stats for set of cities
|
||||
*
|
||||
* Returns total count of views, downloads, unique views and unique downloads in a city
|
||||
*/
|
||||
public function getManyCities(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
|
||||
|
||||
$defaultParams = [
|
||||
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
|
||||
];
|
||||
$initAllowedParams = $this->getManyAllowedParams();
|
||||
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
|
||||
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
|
||||
|
||||
Hook::call('API::stats::publications::cities::params', [&$allowedParams, $slimRequest]);
|
||||
|
||||
// Check/validate, filter and sanitize the request params
|
||||
try {
|
||||
$allowedParams = $this->validateParams($allowedParams);
|
||||
} catch (\Exception $e) {
|
||||
if ($e->getCode() == 200) {
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_CITY);
|
||||
return $response->withCSV([], $csvColumnNames, 0);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => [],
|
||||
'itemsMax' => 0,
|
||||
], 200);
|
||||
}
|
||||
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
|
||||
}
|
||||
|
||||
$statsService = Services::get('geoStats');
|
||||
// Get a list of top cities by total views
|
||||
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_CITY);
|
||||
|
||||
// Get the stats for each city
|
||||
$items = [];
|
||||
$isoCodes = app(IsoCodesFactory::class);
|
||||
foreach ($totals as $total) {
|
||||
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
|
||||
$countryName = $country ? $country->getLocalName() : __('stats.unknown');
|
||||
$regionName = !empty($total->region) ? $total->region : __('stats.unknown');
|
||||
if (!empty($total->country) && !empty($total->region)) {
|
||||
$regionCode = $total->country . '-' . $total->region;
|
||||
$region = $isoCodes->getSubdivisions()->getByCode($regionCode);
|
||||
$regionName = $region ? $region->getLocalName() : $regionCode;
|
||||
}
|
||||
$cityName = !empty($total->city) ? $total->city : __('stats.unknown');
|
||||
|
||||
$metric = $total->metric;
|
||||
$metric_unique = $total->metric_unique;
|
||||
if ($responseCSV) {
|
||||
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName, $regionName, $cityName);
|
||||
} else {
|
||||
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName, $regionName, $cityName);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the total count of cities
|
||||
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_CITY);
|
||||
if ($responseCSV) {
|
||||
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_CITY);
|
||||
return $response->withCSV($items, $csvColumnNames, $itemsMax);
|
||||
}
|
||||
return $response->withJson([
|
||||
'items' => $items,
|
||||
'itemsMax' => $itemsMax,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate, filter, sanitize the params
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function validateParams(array $allowedParams): array
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
$allowedParams['contextIds'] = [$request->getContext()->getId()];
|
||||
|
||||
$result = $this->_validateStatDates($allowedParams);
|
||||
if ($result !== true) {
|
||||
throw new \Exception($result, 400);
|
||||
}
|
||||
|
||||
if (array_key_exists('orderDirection', $allowedParams) && !in_array($allowedParams['orderDirection'], [StatisticsHelper::STATISTICS_ORDER_ASC, StatisticsHelper::STATISTICS_ORDER_DESC])) {
|
||||
throw new \Exception('api.stats.400.invalidOrderDirection', 400);
|
||||
}
|
||||
|
||||
if (array_key_exists('timelineInterval', $allowedParams) && !$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
|
||||
throw new \Exception('api.stats.400.invalidTimelineInterval', 400);
|
||||
}
|
||||
|
||||
// Identify submissions which should be included in the results when a searchPhrase is passed
|
||||
if (!empty($allowedParams['searchPhrase'])) {
|
||||
$allowedSubmissionIds = empty($allowedParams['submissionIds']) ? [] : $allowedParams['submissionIds'];
|
||||
$allowedParams['submissionIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedSubmissionIds);
|
||||
|
||||
if (empty($allowedParams['submissionIds'])) {
|
||||
throw new \Exception('', 200);
|
||||
}
|
||||
unset($allowedParams['searchPhrase']);
|
||||
}
|
||||
return $allowedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter and sanitize the request param
|
||||
*/
|
||||
protected function _processParam(string $requestParam, mixed $value): array
|
||||
{
|
||||
$returnParams = [];
|
||||
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
|
||||
switch ($requestParam) {
|
||||
case 'dateStart':
|
||||
case 'dateEnd':
|
||||
case 'timelineInterval':
|
||||
case 'searchPhrase':
|
||||
case 'type':
|
||||
$returnParams[$requestParam] = $value;
|
||||
break;
|
||||
|
||||
case 'count':
|
||||
$returnParams[$requestParam] = min(100, (int) $value);
|
||||
break;
|
||||
|
||||
case 'offset':
|
||||
$returnParams[$requestParam] = (int) $value;
|
||||
break;
|
||||
|
||||
case 'orderDirection':
|
||||
$returnParams[$requestParam] = strtoupper($value);
|
||||
break;
|
||||
|
||||
case $sectionIdsQueryParam:
|
||||
if (is_string($value) && str_contains($value, ',')) {
|
||||
$value = explode(',', $value);
|
||||
} elseif (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
$returnParams['pkpSectionIds'] = array_map('intval', $value);
|
||||
break;
|
||||
|
||||
case 'submissionIds':
|
||||
if (is_string($value) && str_contains($value, ',')) {
|
||||
$value = explode(',', $value);
|
||||
} elseif (!is_array($value)) {
|
||||
$value = [$value];
|
||||
}
|
||||
$returnParams[$requestParam] = array_map('intval', $value);
|
||||
break;
|
||||
}
|
||||
return $returnParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to filter and sanitize the request params
|
||||
*
|
||||
* Only allows the specified params through and enforces variable
|
||||
* type where needed.
|
||||
*/
|
||||
protected function _processAllowedParams(array $requestParams, array $allowedParams): array
|
||||
{
|
||||
$returnParams = [];
|
||||
foreach ($requestParams as $requestParam => $value) {
|
||||
if (!in_array($requestParam, $allowedParams)) {
|
||||
continue;
|
||||
}
|
||||
$returnParams += $this->_processParam($requestParam, $value);
|
||||
}
|
||||
// Get the context's earliest date of publication if no start date is set
|
||||
if (in_array('dateStart', $allowedParams) && !isset($returnParams['dateStart'])) {
|
||||
$dateRange = Repo::publication()->getDateBoundaries(
|
||||
Repo::publication()
|
||||
->getCollector()
|
||||
->filterByContextIds([$this->getRequest()->getContext()->getId()])
|
||||
);
|
||||
$returnParams['dateStart'] = $dateRange->min_date_published;
|
||||
}
|
||||
return $returnParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to get the submissionIds param when a searchPhase
|
||||
* param is also passed.
|
||||
*
|
||||
* If the searchPhrase and submissionIds params were both passed in the
|
||||
* request, then we only return IDs that match both conditions.
|
||||
*/
|
||||
protected function _processSearchPhrase(string $searchPhrase, array $submissionIds = []): array
|
||||
{
|
||||
$searchPhraseSubmissionIds = Repo::submission()
|
||||
->getCollector()
|
||||
->filterByContextIds([Application::get()->getRequest()->getContext()->getId()])
|
||||
->filterByStatus([Submission::STATUS_PUBLISHED])
|
||||
->searchPhrase($searchPhrase)
|
||||
->getIds();
|
||||
|
||||
if (!empty($submissionIds)) {
|
||||
$submissionIds = array_intersect($submissionIds, $searchPhraseSubmissionIds->toArray());
|
||||
} else {
|
||||
$submissionIds = $searchPhraseSubmissionIds->toArray();
|
||||
}
|
||||
|
||||
return $submissionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column names for the submission CSV report
|
||||
*/
|
||||
protected function _getSubmissionReportColumnNames(): array
|
||||
{
|
||||
return [
|
||||
__('common.id'),
|
||||
__('common.title'),
|
||||
__('stats.total'),
|
||||
__('submission.abstractViews'),
|
||||
__('stats.fileViews'),
|
||||
__('stats.pdf'),
|
||||
__('stats.html'),
|
||||
__('common.other')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column names for the file CSV report
|
||||
*/
|
||||
protected function _getFileReportColumnNames(): array
|
||||
{
|
||||
return [
|
||||
__('common.publication') . ' ' . __('common.id'),
|
||||
__('submission.title'),
|
||||
__('common.file') . ' ' . __('common.id'),
|
||||
__('common.fileName'),
|
||||
__('common.type'),
|
||||
__('stats.fileViews'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column names for the country, region and city CSV report
|
||||
*/
|
||||
protected function _getGeoReportColumnNames(string $scale, bool $withPublication = false): array
|
||||
{
|
||||
$publicationColumns = [];
|
||||
if ($withPublication) {
|
||||
$publicationColumns = [
|
||||
__('common.id'),
|
||||
__('common.title')
|
||||
];
|
||||
}
|
||||
$scaleColumns = [];
|
||||
if ($scale == StatisticsHelper::STATISTICS_DIMENSION_CITY) {
|
||||
$scaleColumns = [
|
||||
__('stats.city'),
|
||||
__('stats.region'),
|
||||
__('common.country')
|
||||
];
|
||||
} elseif ($scale == StatisticsHelper::STATISTICS_DIMENSION_REGION) {
|
||||
$scaleColumns = [__('stats.region'), __('common.country')];
|
||||
} elseif ($scale == StatisticsHelper::STATISTICS_DIMENSION_COUNTRY) {
|
||||
$scaleColumns = [__('common.country'),];
|
||||
}
|
||||
return array_merge(
|
||||
$publicationColumns,
|
||||
$scaleColumns,
|
||||
[__('stats.total'), __('stats.unique')]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CSV row with submission metrics
|
||||
*/
|
||||
protected function getItemForCSV(int $submissionId, int $abstractViews, int $pdfViews, int $htmlViews, int $otherViews): array
|
||||
{
|
||||
$galleyViews = $pdfViews + $htmlViews + $otherViews;
|
||||
$totalViews = $abstractViews + $galleyViews;
|
||||
|
||||
// Get submission title for display
|
||||
$submission = Repo::submission()->get($submissionId);
|
||||
$submissionTitle = $submission->getCurrentPublication()->getLocalizedTitle();
|
||||
|
||||
return [
|
||||
$submissionId,
|
||||
$submissionTitle,
|
||||
$totalViews,
|
||||
$abstractViews,
|
||||
$galleyViews,
|
||||
$pdfViews,
|
||||
$htmlViews,
|
||||
$otherViews
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON data with submission metrics
|
||||
*/
|
||||
protected function getItemForJSON(int $submissionId, int $abstractViews, int $pdfViews, int $htmlViews, int $otherViews): array
|
||||
{
|
||||
$galleyViews = $pdfViews + $htmlViews + $otherViews;
|
||||
|
||||
// Get basic submission details for display
|
||||
$submission = Repo::submission()->get($submissionId);
|
||||
$submissionProps = Repo::submission()->getSchemaMap()->mapToStats($submission);
|
||||
|
||||
return [
|
||||
'abstractViews' => $abstractViews,
|
||||
'galleyViews' => $galleyViews,
|
||||
'pdfViews' => $pdfViews,
|
||||
'htmlViews' => $htmlViews,
|
||||
'otherViews' => $otherViews,
|
||||
'publication' => $submissionProps,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSV row with file metrics
|
||||
*/
|
||||
protected function getFilesCSVItem(int $submissionFileId, int $downloads, int $assocType, int $submissionId, string $submissionTitle): array
|
||||
{
|
||||
// Get submission file title for display
|
||||
$submissionFile = Repo::submissionFile()->get($submissionFileId);
|
||||
$title = $submissionFile->getLocalizedData('name');
|
||||
$type = $assocType == Application::ASSOC_TYPE_SUBMISSION_FILE ? __('stats.file.type.primaryFile') : __('stats.file.type.suppFile');
|
||||
return [
|
||||
$submissionId,
|
||||
$submissionTitle,
|
||||
$submissionFileId,
|
||||
$title,
|
||||
$type,
|
||||
$downloads
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON data with file metrics
|
||||
*/
|
||||
protected function getFilesJSONItem(int $submissionFileId, int $downloads, int $assocType, int $submissionId, string $submissionTitle): array
|
||||
{
|
||||
// Get submission file title for display
|
||||
$submissionFile = Repo::submissionFile()->get($submissionFileId);
|
||||
$title = $submissionFile->getLocalizedData('name');
|
||||
$type = $assocType == Application::ASSOC_TYPE_SUBMISSION_FILE ? __('stats.file.type.primaryFile') : __('stats.file.type.suppFile');
|
||||
return [
|
||||
'submissionId' => $submissionId,
|
||||
'submissionTitle' => $submissionTitle,
|
||||
'submissionFileId' => $submissionFileId,
|
||||
'fileName' => $title,
|
||||
'fileType' => $type,
|
||||
'downloads' => $downloads
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSV row with geographical (country, region, and/or city) metrics
|
||||
*/
|
||||
protected function getGeoCSVItem(int $metric, int $metric_unique, string $country, ?string $region = null, ?string $city = null): array
|
||||
{
|
||||
$item = [];
|
||||
if (isset($city)) {
|
||||
$item[] = $city;
|
||||
}
|
||||
if (isset($region)) {
|
||||
$item[] = $region;
|
||||
}
|
||||
return array_merge($item, [$country, $metric, $metric_unique]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JSON data with geographical (country, region, and/or city) metrics
|
||||
*/
|
||||
protected function getGeoJSONItem(int $metric, int $metric_unique, string $country, ?string $region = null, ?string $city = null): array
|
||||
{
|
||||
$item = [];
|
||||
if (isset($city)) {
|
||||
$item['city'] = $city;
|
||||
}
|
||||
if (isset($region)) {
|
||||
$item['region'] = $region;
|
||||
}
|
||||
return array_merge(
|
||||
$item,
|
||||
[
|
||||
'country' => $country,
|
||||
'total' => $metric,
|
||||
'unique' => $metric_unique
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the timeline interval is valid
|
||||
*/
|
||||
protected function isValidTimelineInterval(string $interval): bool
|
||||
{
|
||||
return in_array($interval, [
|
||||
StatisticsHelper::STATISTICS_DIMENSION_DAY,
|
||||
StatisticsHelper::STATISTICS_DIMENSION_MONTH
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file api/v1/stats/sushi/PKPStatsSushiHandler.php
|
||||
*
|
||||
* Copyright (c) 2022 Simon Fraser University
|
||||
* Copyright (c) 2022 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class PKPStatsSushiHandler
|
||||
*
|
||||
* @ingroup api_v1_stats
|
||||
*
|
||||
* @brief Handle API requests for COUNTER R5 SUSHI statistics.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace PKP\API\v1\stats\sushi;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\facades\Repo;
|
||||
use APP\sushi\PR;
|
||||
use APP\sushi\PR_P1;
|
||||
use PKP\core\APIResponse;
|
||||
use PKP\handler\APIHandler;
|
||||
use PKP\security\authorization\ContextRequiredPolicy;
|
||||
use PKP\security\authorization\PolicySet;
|
||||
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
|
||||
use PKP\security\authorization\UserRolesRequiredPolicy;
|
||||
use PKP\security\Role;
|
||||
use PKP\sushi\CounterR5Report;
|
||||
use PKP\sushi\SushiException;
|
||||
use PKP\validation\ValidatorFactory;
|
||||
use Slim\Http\Request as SlimHttpRequest;
|
||||
|
||||
class PKPStatsSushiHandler extends APIHandler
|
||||
{
|
||||
/** @var bool Whether the API is public */
|
||||
public $isPublic = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$site = Application::get()->getRequest()->getSite();
|
||||
$context = Application::get()->getRequest()->getContext();
|
||||
if (($site->getData('isSushiApiPublic') !== null && !$site->getData('isSushiApiPublic')) ||
|
||||
($context->getData('isSushiApiPublic') !== null && !$context->getData('isSushiApiPublic'))) {
|
||||
$this->isPublic = false;
|
||||
}
|
||||
|
||||
$this->_handlerPath = 'stats/sushi';
|
||||
$roles = $this->isPublic ? null : [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
|
||||
$this->_endpoints = [
|
||||
'GET' => $this->getGETDefinitions($roles)
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this API's endpoints definitions
|
||||
*/
|
||||
protected function getGETDefinitions(array $roles = null): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/status',
|
||||
'handler' => [$this, 'getStatus'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/members',
|
||||
'handler' => [$this, 'getMembers'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/reports',
|
||||
'handler' => [$this, 'getReports'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/reports/pr',
|
||||
'handler' => [$this, 'getReportsPR'],
|
||||
'roles' => $roles
|
||||
],
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern() . '/reports/pr_p1',
|
||||
'handler' => [$this, 'getReportsPR1'],
|
||||
'roles' => $roles
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPHandler::authorize()
|
||||
*/
|
||||
public function authorize($request, &$args, $roleAssignments)
|
||||
{
|
||||
$this->addPolicy(new ContextRequiredPolicy($request));
|
||||
if (!$this->isPublic) {
|
||||
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
|
||||
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
|
||||
foreach ($roleAssignments as $role => $operations) {
|
||||
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
|
||||
}
|
||||
$this->addPolicy($rolePolicy);
|
||||
}
|
||||
return parent::authorize($request, $args, $roleAssignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of the reporting service
|
||||
*/
|
||||
public function getStatus(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
$context = $request->getContext();
|
||||
// use only the name in the context primary locale to be consistent
|
||||
$contextName = $context->getName($context->getPrimaryLocale());
|
||||
return $response->withJson([
|
||||
'Description' => __('sushi.status.description', ['contextName' => $contextName]),
|
||||
'Service_Active' => true,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of consortium members related to a Customer_ID
|
||||
*/
|
||||
public function getMembers(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
$context = $request->getContext();
|
||||
$site = $request->getSite();
|
||||
$params = $slimRequest->getQueryParams();
|
||||
if (!isset($params['customer_id'])) {
|
||||
// error: missing required customer_id
|
||||
return $response->withJson([
|
||||
'Code' => 1030,
|
||||
'Severity' => 'Fatal',
|
||||
'Message' => 'Insufficient Information to Process Request',
|
||||
'Data' => __('sushi.exception.1030.missing', ['params' => 'customer_id'])
|
||||
], 400);
|
||||
}
|
||||
$platformId = $context->getPath();
|
||||
if ($site->getData('isSiteSushiPlatform')) {
|
||||
$platformId = $site->getData('sushiPlatformID');
|
||||
}
|
||||
$institutionName = $institutionId = null;
|
||||
$customerId = $params['customer_id'];
|
||||
if (is_numeric($customerId)) {
|
||||
$customerId = (int) $customerId;
|
||||
if ($customerId == 0) {
|
||||
$institutionName = 'The World';
|
||||
} else {
|
||||
$institution = Repo::institution()->get($customerId);
|
||||
if (isset($institution) && $institution->getContextId() == $context->getId()) {
|
||||
$institutionId = [];
|
||||
$institutionName = $institution->getLocalizedName();
|
||||
if (!empty($institution->getROR())) {
|
||||
$institutionId[] = ['Type' => 'ROR', 'Value' => $institution->getROR()];
|
||||
}
|
||||
$institutionId[] = ['Type' => 'Proprietary', 'Value' => $platformId . ':' . $customerId];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isset($institutionName)) {
|
||||
// error: invalid customer_id
|
||||
return $response->withJson([
|
||||
'Code' => 1030,
|
||||
'Severity' => 'Fatal',
|
||||
'Message' => 'Insufficient Information to Process Request',
|
||||
'Data' => __('sushi.exception.1030.invalid', ['params' => 'customer_id'])
|
||||
], 400);
|
||||
}
|
||||
$item = [
|
||||
'Customer_ID' => $customerId,
|
||||
'Name' => $institutionName,
|
||||
];
|
||||
if (isset($institutionId)) {
|
||||
$item['Institution_ID'] = $institutionId;
|
||||
}
|
||||
return $response->withJson([$item], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of reports supported by the API
|
||||
*/
|
||||
public function getReports(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$items = $this->getReportList();
|
||||
return $response->withJson($items, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application specific list of reports supported by the API
|
||||
*/
|
||||
protected function getReportList(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'Report_Name' => 'Platform Master Report',
|
||||
'Report_ID' => 'PR',
|
||||
'Release' => '5',
|
||||
'Report_Description' => __('sushi.reports.pr.description'),
|
||||
'Path' => 'reports/pr'
|
||||
],
|
||||
[
|
||||
'Report_Name' => 'Platform Usage',
|
||||
'Report_ID' => 'PR_P1',
|
||||
'Release' => '5',
|
||||
'Report_Description' => __('sushi.reports.pr_p1.description'),
|
||||
'Path' => 'reports/pr_p1'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* COUNTER 'Platform Usage' [PR_P1].
|
||||
* A customizable report summarizing activity across the Platform (journal, press, or server).
|
||||
*/
|
||||
public function getReportsPR(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
return $this->getReportResponse(new PR(), $slimRequest, $response, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* COUNTER 'Platform Master Report' [PR].
|
||||
* This is a Standard View of the Platform Master Report that presents usage for the overall Platform broken down by Metric_Type
|
||||
*/
|
||||
public function getReportsPR1(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
return $this->getReportResponse(new PR_P1(), $slimRequest, $response, $args);
|
||||
}
|
||||
|
||||
/** Validate user input for TSV reports */
|
||||
protected function _validateUserInput(CounterR5Report $report, array $params): array
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
$context = $request->getContext();
|
||||
$earliestDate = CounterR5Report::getEarliestDate();
|
||||
$lastDate = CounterR5Report::getLastDate();
|
||||
$submissionIds = Repo::submission()->getCollector()->filterByContextIds([$context->getId()])->getIds()->implode(',');
|
||||
|
||||
$rules = [
|
||||
'begin_date' => [
|
||||
'regex:/^\d{4}-\d{2}(-\d{2})?$/',
|
||||
'after_or_equal:' . $earliestDate,
|
||||
'before_or_equal:end_date',
|
||||
],
|
||||
'end_date' => [
|
||||
'regex:/^\d{4}-\d{2}(-\d{2})?$/',
|
||||
'before_or_equal:' . $lastDate,
|
||||
'after_or_equal:begin_date',
|
||||
],
|
||||
'item_id' => [
|
||||
// TO-ASK: shell this rather be just validation for positive integer?
|
||||
'in:' . $submissionIds,
|
||||
],
|
||||
'yop' => [
|
||||
'regex:/^\d{4}((\||-)\d{4})*$/',
|
||||
],
|
||||
];
|
||||
$reportId = $report->getID();
|
||||
if (in_array($reportId, ['PR', 'TR', 'IR'])) {
|
||||
$rules['metric_type'] = ['required'];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$validator = ValidatorFactory::make(
|
||||
$params,
|
||||
$rules,
|
||||
[
|
||||
'begin_date.regex' => __(
|
||||
'manager.statistics.counterR5Report.settings.wrongDateFormat'
|
||||
),
|
||||
'end_date.regex' => __(
|
||||
'manager.statistics.counterR5Report.settings.wrongDateFormat'
|
||||
),
|
||||
'begin_date.after_or_equal' => __(
|
||||
'stats.dateRange.invalidStartDateMin'
|
||||
),
|
||||
'end_date.before_or_equal' => __(
|
||||
'stats.dateRange.invalidEndDateMax'
|
||||
),
|
||||
'begin_date.before_or_equal' => __(
|
||||
'stats.dateRange.invalidDateRange'
|
||||
),
|
||||
'end_date.after_or_equal' => __(
|
||||
'stats.dateRange.invalidDateRange'
|
||||
),
|
||||
'item_id.*' => __(
|
||||
'manager.statistics.counterR5Report.settings.wrongItemId'
|
||||
),
|
||||
'yop.regex' => __(
|
||||
'manager.statistics.counterR5Report.settings.wrongYOPFormat'
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors = $validator->errors()->getMessages();
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the requested report
|
||||
*/
|
||||
protected function getReportResponse(CounterR5Report $report, SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
|
||||
{
|
||||
$responseTSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_TSV) ? true : false;
|
||||
|
||||
$params = $slimRequest->getQueryParams();
|
||||
|
||||
if ($responseTSV) {
|
||||
$errors = $this->_validateUserInput($report, $params);
|
||||
if (!empty($errors)) {
|
||||
return $response->withJson($errors, 400);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$report->processReportParams($this->getRequest(), $params);
|
||||
} catch (SushiException $e) {
|
||||
return $response->withJson($e->getResponseData(), $e->getHttpStatusCode());
|
||||
}
|
||||
|
||||
if ($responseTSV) {
|
||||
$reportHeader = $report->getTSVReportHeader();
|
||||
$reportColumnNames = $report->getTSVColumnNames();
|
||||
$reportItems = $report->getTSVReportItems();
|
||||
// consider 3030 error (no usage available)
|
||||
$key = array_search('3030', array_column($report->warnings, 'Code'));
|
||||
if ($key !== false) {
|
||||
$error = $report->warnings[$key]['Code'] . ':' . $report->warnings[$key]['Message'] . '(' . $report->warnings[$key]['Data'] . ')';
|
||||
foreach ($reportHeader as &$headerRow) {
|
||||
if (in_array('Exceptions', $headerRow)) {
|
||||
$headerRow[1] =
|
||||
$headerRow[1] == '' ?
|
||||
$error :
|
||||
$headerRow[1] . ';' . $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
$report = array_merge($reportHeader, [['']], $reportColumnNames, $reportItems);
|
||||
return $response->withCSV($report, [], count($reportItems), APIResponse::RESPONSE_TSV);
|
||||
}
|
||||
|
||||
$reportHeader = $report->getReportHeader();
|
||||
$reportItems = $report->getReportItems();
|
||||
return $response->withJson([
|
||||
'Report_Header' => $reportHeader,
|
||||
'Report_Items' => $reportItems,
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file api/v1/stats/users/PKPStatsUserHandler.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 PKPStatsUserHandler
|
||||
*
|
||||
* @ingroup api_v1_stats
|
||||
*
|
||||
* @brief Handle API requests for publication statistics.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace PKP\API\v1\stats\users;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use PKP\handler\APIHandler;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\security\authorization\ContextAccessPolicy;
|
||||
use PKP\security\authorization\PolicySet;
|
||||
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
|
||||
use PKP\security\authorization\UserRolesRequiredPolicy;
|
||||
use PKP\security\Role;
|
||||
|
||||
class PKPStatsUserHandler extends APIHandler
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->_handlerPath = 'stats/users';
|
||||
$this->_endpoints = [
|
||||
'GET' => [
|
||||
[
|
||||
'pattern' => $this->getEndpointPattern(),
|
||||
'handler' => [$this, 'get'],
|
||||
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
|
||||
],
|
||||
],
|
||||
];
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPHandler::authorize()
|
||||
*/
|
||||
public function authorize($request, &$args, $roleAssignments)
|
||||
{
|
||||
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
|
||||
|
||||
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
|
||||
|
||||
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
|
||||
foreach ($roleAssignments as $role => $operations) {
|
||||
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
|
||||
}
|
||||
$this->addPolicy($rolePolicy);
|
||||
|
||||
return parent::authorize($request, $args, $roleAssignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user stats
|
||||
*
|
||||
* Returns the count of users broken down by roles
|
||||
*
|
||||
* @param \Slim\Http\Request $slimRequest Slim request object
|
||||
* @param object $response Response
|
||||
* @param array $args
|
||||
*
|
||||
* @return object Response
|
||||
*/
|
||||
public function get($slimRequest, $response, $args)
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
if (!$request->getContext()) {
|
||||
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
|
||||
}
|
||||
|
||||
$collector = Repo::user()->getCollector();
|
||||
$dateParams = [];
|
||||
foreach ($slimRequest->getQueryParams() as $param => $value) {
|
||||
switch ($param) {
|
||||
case 'registeredAfter':
|
||||
$collector->filterRegisteredAfter($value);
|
||||
$dateParams['dateStart'] = $value;
|
||||
break;
|
||||
case 'registeredBefore':
|
||||
$collector->filterRegisteredBefore($value);
|
||||
$dateParams['dateEnd'] = $value;
|
||||
break;
|
||||
case 'status': switch ($value) {
|
||||
case 'disabled':
|
||||
$collector->filterByStatus($collector::STATUS_DISABLED);
|
||||
break;
|
||||
case 'all':
|
||||
$collector->filterByStatus($collector::STATUS_ALL);
|
||||
break;
|
||||
default:
|
||||
case 'active':
|
||||
$collector->filterByStatus($collector::STATUS_ACTIVE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Hook::call('API::stats::users::params', [$collector, $slimRequest]);
|
||||
|
||||
$collector->filterByContextIds([$request->getContext()->getId()]);
|
||||
|
||||
$result = $this->_validateStatDates($dateParams);
|
||||
if ($result !== true) {
|
||||
return $response->withStatus(400)->withJsonError($result);
|
||||
}
|
||||
|
||||
return $response->withJson(array_map(
|
||||
function ($item) {
|
||||
$item['name'] = __($item['name']);
|
||||
return $item;
|
||||
},
|
||||
Repo::user()->getRolesOverview($collector)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user