first commit
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/services/QueryBuilders/PKPContextQueryBuilder.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class PKPContextQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Base class for context (journals/presses) list query builder
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\security\Role;
|
||||
use PKP\services\queryBuilders\interfaces\EntityQueryBuilderInterface;
|
||||
|
||||
abstract class PKPContextQueryBuilder implements EntityQueryBuilderInterface
|
||||
{
|
||||
/** @var string The database name for this context: `journals` or `presses` */
|
||||
protected $db;
|
||||
|
||||
/** @var string The database name for this context's settings: `journal_settings` or `press_settings` */
|
||||
protected $dbSettings;
|
||||
|
||||
/** @var string The column name for a context ID: `journal_id` or `press_id` */
|
||||
protected $dbIdColumn;
|
||||
|
||||
/** @var ?bool enabled or disabled contexts */
|
||||
protected $isEnabled = null;
|
||||
|
||||
/** @var ?int Filter contexts by whether or not this user can access it when logged in */
|
||||
protected $userId;
|
||||
|
||||
/** @var ?string search phrase */
|
||||
protected $searchPhrase = null;
|
||||
|
||||
/** @var string[] Selected columns */
|
||||
protected $columns = [];
|
||||
|
||||
/**
|
||||
* Set isEnabled filter
|
||||
*
|
||||
* @param ?bool $isEnabled
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPContextQueryBuilder
|
||||
*/
|
||||
public function filterByIsEnabled($isEnabled)
|
||||
{
|
||||
$this->isEnabled = $isEnabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userId filter
|
||||
*
|
||||
* The user id can access contexts where they are assigned to
|
||||
* a user group. If the context is disabled, they must be
|
||||
* assigned to ROLE_ID_MANAGER user group.
|
||||
*
|
||||
* @param ?int $userId
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPContextQueryBuilder
|
||||
*/
|
||||
public function filterByUserId($userId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set query search phrase
|
||||
*
|
||||
* @param ?string $phrase
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPContextQueryBuilder
|
||||
*/
|
||||
public function searchPhrase($phrase)
|
||||
{
|
||||
$this->searchPhrase = $phrase;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKP\services\queryBuilders\interfaces\EntityQueryBuilderInterface::getCount()
|
||||
*/
|
||||
public function getCount()
|
||||
{
|
||||
return $this
|
||||
->getQuery()
|
||||
->select('c.' . $this->dbIdColumn)
|
||||
->get()
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKP\services\queryBuilders\interfaces\EntityQueryBuilderInterface::getIds()
|
||||
*/
|
||||
public function getIds()
|
||||
{
|
||||
return $this
|
||||
->getQuery()
|
||||
->select('c.' . $this->dbIdColumn)
|
||||
->pluck('c.' . $this->dbIdColumn)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name and basic data for a set of contexts
|
||||
*
|
||||
* This returns data from the main table and the name
|
||||
* of the context in its primary locale.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getManySummary()
|
||||
{
|
||||
return $this
|
||||
->getQuery()
|
||||
->select([
|
||||
'c.' . $this->dbIdColumn . ' as id',
|
||||
'c.enabled',
|
||||
'cst.setting_value as name',
|
||||
'c.path as urlPath',
|
||||
'c.seq',
|
||||
])
|
||||
->leftJoin($this->dbSettings . ' as cst', function ($q) {
|
||||
$q->where('cst.' . $this->dbIdColumn, '=', DB::raw('c.' . $this->dbIdColumn))
|
||||
->where('cst.setting_name', '=', 'name')
|
||||
->where('cst.locale', '=', DB::raw('c.primary_locale'));
|
||||
})
|
||||
->orderBy('c.seq')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKP\services\queryBuilders\interfaces\EntityQueryBuilderInterface::getQuery()
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
$this->columns[] = 'c.*';
|
||||
$q = DB::table($this->db . ' as c');
|
||||
|
||||
if (!empty($this->isEnabled)) {
|
||||
$q->where('c.enabled', '=', 1);
|
||||
} elseif ($this->isEnabled === false) {
|
||||
$q->where('c.enabled', '!=', 1);
|
||||
}
|
||||
|
||||
// Filter for user id if present
|
||||
$q->when(!empty($this->userId), function ($q) {
|
||||
$q->whereIn('c.' . $this->dbIdColumn, function ($q) {
|
||||
$q->select('context_id')
|
||||
->from('user_groups')
|
||||
->where(function ($q) {
|
||||
$q->where('role_id', '=', Role::ROLE_ID_MANAGER)
|
||||
->orWhere('c.enabled', '=', 1);
|
||||
})
|
||||
->whereIn('user_group_id', function ($q) {
|
||||
$q->select('user_group_id')
|
||||
->from('user_user_groups')
|
||||
->where('user_id', '=', $this->userId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// search phrase
|
||||
$q->when($this->searchPhrase !== null, function ($query) {
|
||||
$words = explode(' ', $this->searchPhrase);
|
||||
foreach ($words as $word) {
|
||||
$query->whereIn('c.' . $this->dbIdColumn, function ($query) use ($word) {
|
||||
return $query->select($this->dbIdColumn)
|
||||
->from($this->dbSettings)
|
||||
->whereIn('setting_name', ['description', 'acronym', 'abbreviation'])
|
||||
->where(DB::raw('LOWER(setting_value)'), 'LIKE', DB::raw("CONCAT('%', LOWER(?), '%')"))->addBinding($word);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add app-specific query statements
|
||||
Hook::call('Context::getContexts::queryObject', [&$q, $this]);
|
||||
$q->select($this->columns);
|
||||
|
||||
return $q;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/queryBuilders/PKPStatsContextQueryBuilder.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 PKPStatsContextQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Helper class to construct a query to fetch context stats records from the
|
||||
* metrics_context table.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\statistics\PKPStatisticsHelper;
|
||||
|
||||
class PKPStatsContextQueryBuilder extends PKPStatsQueryBuilder
|
||||
{
|
||||
/**
|
||||
* Get contexts IDs
|
||||
*/
|
||||
public function getContextIds(): Builder
|
||||
{
|
||||
return $this->_getObject()
|
||||
->select([PKPStatisticsHelper::STATISTICS_DIMENSION_CONTEXT_ID])
|
||||
->distinct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::_getObject()
|
||||
*/
|
||||
protected function _getObject(): Builder
|
||||
{
|
||||
$q = DB::table('metrics_context');
|
||||
|
||||
if (!empty($this->contextIds)) {
|
||||
$q->whereIn(PKPStatisticsHelper::STATISTICS_DIMENSION_CONTEXT_ID, $this->contextIds);
|
||||
}
|
||||
|
||||
$q->whereBetween(PKPStatisticsHelper::STATISTICS_DIMENSION_DATE, [$this->dateStart, $this->dateEnd]);
|
||||
|
||||
if ($this->limit > 0) {
|
||||
$q->limit($this->limit);
|
||||
if ($this->offset > 0) {
|
||||
$q->offset($this->offset);
|
||||
}
|
||||
}
|
||||
|
||||
Hook::call('StatsContext::queryObject', [&$q, $this]);
|
||||
|
||||
return $q;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/QueryBuilders/PKPStatsEditorialQueryBuilder.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class PKPStatsEditorialQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Helper class to construct a query to fetch stats records from the
|
||||
* metrics table.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use APP\facades\Repo;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\config\Config;
|
||||
use PKP\decision\DecisionType;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\submission\PKPSubmission;
|
||||
|
||||
abstract class PKPStatsEditorialQueryBuilder
|
||||
{
|
||||
/** @var array Return stats for activity in these contexts */
|
||||
protected $contextIds = [];
|
||||
|
||||
/** @var string Return stats for activity before this date */
|
||||
protected $dateEnd;
|
||||
|
||||
/** @var string Return stats for activity after this date */
|
||||
protected $dateStart;
|
||||
|
||||
/** @var array Return stats for activity in these sections (series in OMP) */
|
||||
protected $sectionIds = [];
|
||||
|
||||
/** @var string The table column name for section IDs (OJS) or series IDs (OMP) */
|
||||
public $sectionIdsColumn;
|
||||
|
||||
/**
|
||||
* Set the contexts to return activity for
|
||||
*
|
||||
* @param array|int $contextIds
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPStatsEditorialQueryBuilder
|
||||
*/
|
||||
public function filterByContexts($contextIds)
|
||||
{
|
||||
$this->contextIds = is_array($contextIds) ? $contextIds : [$contextIds];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the section ids to include activity for. This is stored under
|
||||
* the section_id db column but in OMP refers to seriesIds.
|
||||
*
|
||||
* @param array|int $sectionIds
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPStatsEditorialQueryBuilder
|
||||
*/
|
||||
public function filterBySections($sectionIds)
|
||||
{
|
||||
$this->sectionIds = is_array($sectionIds) ? $sectionIds : [$sectionIds];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the date to get activity before
|
||||
*
|
||||
* @param string $dateEnd YYYY-MM-DD
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPStatsEditorialQueryBuilder
|
||||
*/
|
||||
public function before($dateEnd)
|
||||
{
|
||||
$this->dateEnd = $dateEnd;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the date to get activity after
|
||||
*
|
||||
* @param string $dateStart YYYY-MM-DD
|
||||
*
|
||||
* @return \PKP\services\queryBuilders\PKPStatsEditorialQueryBuilder
|
||||
*/
|
||||
public function after($dateStart)
|
||||
{
|
||||
$this->dateStart = $dateStart;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of submissions received
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countSubmissionsReceived()
|
||||
{
|
||||
$q = $this->_getObject();
|
||||
if ($this->dateStart) {
|
||||
$q->where('s.date_submitted', '>=', $this->dateStart);
|
||||
}
|
||||
if ($this->dateEnd) {
|
||||
$q->where('s.date_submitted', '<=', $this->dateEnd);
|
||||
}
|
||||
|
||||
return $q->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of submissions that have received one or more
|
||||
* editor decisions
|
||||
*
|
||||
* @param array $decisions One or more Decision::*
|
||||
* @param bool $forSubmittedDate How date restrictions should be applied.
|
||||
* A false value will count the number of submissions with an editorial
|
||||
* decision within the date range. A true value will count the number of
|
||||
* submissions received within the date range which eventually received
|
||||
* an editorial decision.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countByDecisions($decisions, $forSubmittedDate = false)
|
||||
{
|
||||
$q = $this->_getObject();
|
||||
$q->leftJoin('edit_decisions as ed', 's.submission_id', '=', 'ed.submission_id')
|
||||
->whereIn('ed.decision', $decisions);
|
||||
|
||||
if ($forSubmittedDate) {
|
||||
if ($this->dateStart) {
|
||||
$q->where('s.date_submitted', '>=', $this->dateStart);
|
||||
}
|
||||
if ($this->dateEnd) {
|
||||
// Include date time values up to the end of the day
|
||||
$dateTime = new \DateTime($this->dateEnd);
|
||||
$dateTime->add(new \DateInterval('P1D'));
|
||||
$q->where('s.date_submitted', '<', $dateTime->format('Y-m-d'));
|
||||
}
|
||||
} else {
|
||||
if ($this->dateStart) {
|
||||
$q->where('ed.date_decided', '>=', $this->dateStart);
|
||||
}
|
||||
if ($this->dateEnd) {
|
||||
// Include date time values up to the end of the day
|
||||
$dateTime = new \DateTime($this->dateEnd);
|
||||
$dateTime->add(new \DateInterval('P1D'));
|
||||
$q->where('ed.date_decided', '<', $dateTime->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the decisions being counted have not been
|
||||
// reversed. For example, a submission may have been accepted
|
||||
// and then later declined. We check the current status to
|
||||
// exclude submissions where the status doesn't match the
|
||||
// decisions we are looking for.
|
||||
$declineDecisions = array_map(function (DecisionType $decisionType) {
|
||||
return $decisionType->getDecision();
|
||||
}, Repo::decision()->getDeclineDecisionTypes());
|
||||
if (count(array_intersect($declineDecisions, $decisions))) {
|
||||
$q->where('s.status', '=', PKPSubmission::STATUS_DECLINED);
|
||||
} else {
|
||||
$q->where('s.status', '!=', PKPSubmission::STATUS_DECLINED);
|
||||
}
|
||||
|
||||
$q->select(DB::raw('COUNT(DISTINCT s.submission_id) as count'));
|
||||
|
||||
return $q->get()->first()->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of submissions by one or more status
|
||||
*
|
||||
* @param int|array $status One or more of PKPSubmission::STATUS_*
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countByStatus($status)
|
||||
{
|
||||
return $this->_getObject()
|
||||
->whereIn('s.status', (array) $status)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of active submissions by one or more stages
|
||||
*
|
||||
* @param array $stages One or more of WORKFLOW_STAGE_ID_*
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countActiveByStages($stages)
|
||||
{
|
||||
return $this->_getObject()
|
||||
->where('s.status', '=', PKPSubmission::STATUS_QUEUED)
|
||||
->whereIn('s.stage_id', $stages)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of published submissions
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countPublished()
|
||||
{
|
||||
$q = $this->_getObject()
|
||||
->where('s.status', '=', PKPSubmission::STATUS_PUBLISHED);
|
||||
|
||||
// Only match against the publication date of a
|
||||
// submission's first published publication so
|
||||
// that updated versions are excluded.
|
||||
if ($this->dateStart || $this->dateEnd) {
|
||||
$q->leftJoin('publications as p', function ($q) {
|
||||
$q->where('p.publication_id', function ($q) {
|
||||
$q->from('publications as p2')
|
||||
->where('p2.submission_id', '=', DB::raw('s.submission_id'))
|
||||
->where('p2.status', '=', PKPSubmission::STATUS_PUBLISHED)
|
||||
->orderBy('p2.date_published', 'ASC')
|
||||
->limit(1)
|
||||
->select('p2.publication_id');
|
||||
});
|
||||
});
|
||||
if ($this->dateStart) {
|
||||
$q->where('p.date_published', '>=', $this->dateStart);
|
||||
}
|
||||
if ($this->dateEnd) {
|
||||
$q->where('p.date_published', '<=', $this->dateEnd);
|
||||
}
|
||||
}
|
||||
|
||||
return $q->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of days to reach a particular editor decision
|
||||
*
|
||||
* This list includes any completed submission which has received
|
||||
* one of the editor decisions.
|
||||
*
|
||||
* @param array $decisions One or more Decision::*
|
||||
*
|
||||
* @return array Days between submission and the first decision in
|
||||
* the list of requested submissions
|
||||
*/
|
||||
public function getDaysToDecisions($decisions)
|
||||
{
|
||||
$q = $this->_getDaysToDecisionsObject($decisions);
|
||||
$dateDiff = $this->_dateDiff('ed.date_decided', 's.date_submitted');
|
||||
$q->select(DB::raw($dateDiff . ' as time'));
|
||||
return $q->pluck('time')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the average number of days to reach a particular
|
||||
* editor decision
|
||||
*
|
||||
* This average includes any completed submission which has received
|
||||
* one of the editor decisions.
|
||||
*
|
||||
* @param array $decisions One or more Decision::*
|
||||
*
|
||||
* @return float Average days between submission and the first decision
|
||||
* in the list of requested submissions
|
||||
*/
|
||||
public function getAverageDaysToDecisions($decisions)
|
||||
{
|
||||
$q = $this->_getDaysToDecisionsObject($decisions);
|
||||
$dateDiff = $this->_dateDiff('ed.date_decided', 's.date_submitted');
|
||||
$q->select(DB::raw('AVG(' . $dateDiff . ') as average'));
|
||||
return $q->get()->first()->average;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first and last date of submissions received
|
||||
*
|
||||
* @return array [min, max]
|
||||
*/
|
||||
public function getSubmissionsReceivedDates()
|
||||
{
|
||||
$q = $this->_getObject();
|
||||
return [$q->min('s.date_submitted'), $q->max('s.date_submitted')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first and last date of submissions published
|
||||
*
|
||||
* @return array [min, max]
|
||||
*/
|
||||
public function getPublishedDates()
|
||||
{
|
||||
$q = $this->_getObject()
|
||||
->where('s.status', '=', PKPSubmission::STATUS_PUBLISHED)
|
||||
// Only match against the publication date of a
|
||||
// submission's first published publication so
|
||||
// that updated versions are excluded.
|
||||
->leftJoin('publications as p', function ($q) {
|
||||
$q->where('p.publication_id', function ($q) {
|
||||
$q->from('publications as p2')
|
||||
->where('p2.submission_id', '=', DB::raw('s.submission_id'))
|
||||
->where('p2.status', '=', PKPSubmission::STATUS_PUBLISHED)
|
||||
->orderBy('p2.date_published', 'ASC')
|
||||
->limit(1)
|
||||
->select('p2.publication_id');
|
||||
});
|
||||
});
|
||||
|
||||
return [$q->min('p.date_published'), $q->max('p.date_published')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first and last date that an editorial decision was made
|
||||
*
|
||||
* @param array $decisions One or more Decision::*
|
||||
*
|
||||
* @return array [min, max]
|
||||
*/
|
||||
public function getDecisionsDates($decisions)
|
||||
{
|
||||
$q = $this->_getObject();
|
||||
$q->leftJoin('edit_decisions as ed', 's.submission_id', '=', 'ed.submission_id')
|
||||
->whereIn('ed.decision', $decisions);
|
||||
|
||||
// Ensure that the decisions being counted have not been
|
||||
// reversed. For example, a submission may have been accepted
|
||||
// and then later declined. We check the current status to
|
||||
// exclude submissions where the status doesn't match the
|
||||
// decisions we are looking for.
|
||||
$declineDecisions = array_map(function (DecisionType $decisionType) {
|
||||
return $decisionType->getDecision();
|
||||
}, Repo::decision()->getDeclineDecisionTypes());
|
||||
if (count(array_intersect($declineDecisions, $decisions))) {
|
||||
$q->where('s.status', '=', PKPSubmission::STATUS_DECLINED);
|
||||
} else {
|
||||
$q->where('s.status', '!=', PKPSubmission::STATUS_DECLINED);
|
||||
}
|
||||
|
||||
return [$q->min('ed.date_decided'), $q->max('ed.date_decided')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a base query object with context and section filters.
|
||||
*/
|
||||
protected function _getBaseQuery(): Builder
|
||||
{
|
||||
$q = DB::table('submissions as s');
|
||||
if (!empty($this->contextIds)) {
|
||||
$q->whereIn('s.context_id', $this->contextIds);
|
||||
}
|
||||
if (!empty($this->sectionIds)) {
|
||||
$q->leftJoin('publications as ps', 's.current_publication_id', '=', 'ps.publication_id')
|
||||
->whereIn("ps.{$this->sectionIdsColumn}", $this->sectionIds)
|
||||
->whereNotNull('ps.publication_id');
|
||||
}
|
||||
|
||||
// First publication included to flag imported submissions through heuristics
|
||||
$q->leftJoin(
|
||||
'publications as pi',
|
||||
fn (Builder $q) => $q->where(
|
||||
'pi.publication_id',
|
||||
fn (Builder $q) => $q->from('publications as pi2')
|
||||
->whereColumn('pi2.submission_id', '=', 's.submission_id')
|
||||
->where('pi2.status', '=', PKPSubmission::STATUS_PUBLISHED)
|
||||
->orderBy('pi2.date_published', 'ASC')
|
||||
->limit(1)
|
||||
->select('pi2.publication_id')
|
||||
)
|
||||
);
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a query object based on the configured conditions.
|
||||
* Incomplete and imported submissions are excluded.
|
||||
*
|
||||
* The dateStart and dateEnd filters are not handled here because
|
||||
* the dates must be applied differently for each set of data.
|
||||
*/
|
||||
protected function _getObject(): Builder
|
||||
{
|
||||
$q = $this->_getBaseQuery();
|
||||
|
||||
// Exclude incomplete submissions
|
||||
$q->where('s.submission_progress', '=', 0);
|
||||
|
||||
// Exclude submissions when the date_submitted is later
|
||||
// than the first date_published. This prevents imported
|
||||
// submissions from being counted in editorial stats.
|
||||
$q->where(
|
||||
fn (Builder $q) => $q->whereNull('pi.date_published')
|
||||
->orWhere(DB::raw('CAST(s.date_submitted AS DATE)'), '<=', DB::raw('pi.date_published'))
|
||||
);
|
||||
|
||||
Hook::call('Stats::editorial::queryObject', [&$q, $this]);
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a query object to get a submission's first
|
||||
* decision of the requested decision types
|
||||
*
|
||||
* Pass an empty $decisions array to return the number of days to
|
||||
* _any_ decision.
|
||||
*
|
||||
* @param array $decisions One or more Decision::*
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
protected function _getDaysToDecisionsObject($decisions)
|
||||
{
|
||||
$q = $this->_getObject();
|
||||
|
||||
$q->leftJoin('edit_decisions as ed', function ($q) use ($decisions) {
|
||||
$q->where('ed.edit_decision_id', function ($q) use ($decisions) {
|
||||
$q->from('edit_decisions as ed2')
|
||||
->where('ed2.submission_id', '=', DB::raw('s.submission_id'));
|
||||
if (!empty($decisions)) {
|
||||
$q->whereIn('ed2.decision', $decisions);
|
||||
}
|
||||
$q->orderBy('ed2.date_decided', 'ASC')
|
||||
->limit(1)
|
||||
->select('ed2.edit_decision_id');
|
||||
});
|
||||
});
|
||||
|
||||
$q->whereNotNull('ed.submission_id')
|
||||
->whereNotNull('s.date_submitted');
|
||||
|
||||
if ($this->dateStart) {
|
||||
$q->where('s.date_submitted', '>=', $this->dateStart);
|
||||
}
|
||||
if ($this->dateEnd) {
|
||||
// Include date time values up to the end of the day
|
||||
$dateTime = new \DateTime($this->dateEnd);
|
||||
$dateTime->add(new \DateInterval('P1D'));
|
||||
$q->where('s.date_submitted', '<=', $dateTime->format('Y-m-d'));
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of imported submissions.
|
||||
* Not counted by static::countSubmissionsReceived().
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countImported()
|
||||
{
|
||||
return $this->_getBaseQuery()
|
||||
->where(DB::raw('CAST(s.date_submitted AS DATE)'), '>', DB::raw('pi.date_published'))
|
||||
->when($this->dateStart, fn (Builder $q) => $q->where('s.date_submitted', '>=', $this->dateStart))
|
||||
->when($this->dateEnd, fn (Builder $q) => $q->where('s.date_submitted', '<=', $this->dateEnd))
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of incomplete submissions.
|
||||
* Not counted by static::countSubmissionsReceived().
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countInProgress()
|
||||
{
|
||||
return $this->_getBaseQuery()
|
||||
->where('s.submission_progress', '<>', 0)
|
||||
->when($this->dateStart, fn (Builder $q) => $q->where('s.date_submitted', '>=', $this->dateStart))
|
||||
->when($this->dateEnd, fn (Builder $q) => $q->where('s.date_submitted', '<=', $this->dateEnd))
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of submissions skipped by the other statistics
|
||||
*/
|
||||
public function countSkipped(): int
|
||||
{
|
||||
return $this->countInProgress() + $this->countImported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a suitable diff by days clause according to the active database driver
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function _dateDiff(string $leftDate, string $rightDate)
|
||||
{
|
||||
switch (Config::getVar('database', 'driver')) {
|
||||
case 'mysql':
|
||||
case 'mysqli':
|
||||
return 'DATEDIFF(' . $leftDate . ',' . $rightDate . ')';
|
||||
}
|
||||
return "DATE_PART('day', " . $leftDate . ' - ' . $rightDate . ')';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/queryBuilders/PKPStatsGeoQueryBuilder.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 PKPStatsGeoQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Helper class to construct a query to fetch geographic stats records from the
|
||||
* metrics_submission_geo_monthly table.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use APP\statistics\StatisticsHelper;
|
||||
use APP\submission\Submission;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\config\Config;
|
||||
use PKP\plugins\Hook;
|
||||
|
||||
abstract class PKPStatsGeoQueryBuilder extends PKPStatsQueryBuilder
|
||||
{
|
||||
/** Include records for these sections/series */
|
||||
protected array $pkpSectionIds = [];
|
||||
|
||||
/** Include records for these submissions */
|
||||
protected array $submissionIds = [];
|
||||
|
||||
/** Include records for these countries */
|
||||
protected array $countries = [];
|
||||
|
||||
/** Include records for these regions */
|
||||
protected array $regions = [];
|
||||
|
||||
/** Include records for these cities */
|
||||
protected array $cities = [];
|
||||
|
||||
/** Application specific name of the section column */
|
||||
abstract public function getSectionColumn(): string;
|
||||
|
||||
/**
|
||||
* Set the sections/series to get records for
|
||||
*/
|
||||
public function filterByPKPSections(array $pkpSectionIds): self
|
||||
{
|
||||
$this->pkpSectionIds = $pkpSectionIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the submission to get records for
|
||||
*/
|
||||
public function filterBySubmissions(array $submissionIds): self
|
||||
{
|
||||
$this->submissionIds = $submissionIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the countries to get records for
|
||||
*/
|
||||
public function filterByCountries(array $countries): self
|
||||
{
|
||||
$this->countries = $countries;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the regions to get records for
|
||||
*/
|
||||
public function filterByRegions(array $regions): self
|
||||
{
|
||||
$this->regions = $regions;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cities to get records for
|
||||
*/
|
||||
public function filterByCities(array $cities): self
|
||||
{
|
||||
$this->cities = $cities;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Geo data
|
||||
*/
|
||||
public function getGeoData(array $groupBy): Builder
|
||||
{
|
||||
return $this->_getObject()
|
||||
->select($groupBy)
|
||||
->groupBy($groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::getSum()
|
||||
*/
|
||||
public function getSum(array $groupBy = []): Builder
|
||||
{
|
||||
$q = $this->_getObject();
|
||||
// Build the select and group by clauses.
|
||||
if (!empty($groupBy)) {
|
||||
$q->select($groupBy);
|
||||
$q->groupBy($groupBy);
|
||||
}
|
||||
$q->addSelect(DB::raw('SUM(metric) AS metric'));
|
||||
$q->addSelect(DB::raw('SUM(metric_unique) AS metric_unique'));
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider/add application specific queries
|
||||
*/
|
||||
protected function _getAppSpecificQuery(Builder &$q): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::_getObject()
|
||||
*/
|
||||
protected function _getObject(): Builder
|
||||
{
|
||||
// consider only monthly DB table
|
||||
$q = DB::table('metrics_submission_geo_monthly');
|
||||
|
||||
if (!empty($this->contextIds)) {
|
||||
$q->whereIn(StatisticsHelper::STATISTICS_DIMENSION_CONTEXT_ID, $this->contextIds);
|
||||
}
|
||||
|
||||
if (!empty($this->submissionIds)) {
|
||||
$q->whereIn(StatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID, $this->submissionIds);
|
||||
}
|
||||
|
||||
if (!empty($this->countries)) {
|
||||
$q->whereIn(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY, $this->countries);
|
||||
}
|
||||
|
||||
if (!empty($this->regions)) {
|
||||
// get first region (so that we can use where and then orWhere query)
|
||||
$fistCountryRegionCode = array_shift($this->regions);
|
||||
// regions must be in a form countryCode-regionCode
|
||||
[$country, $region] = explode('-', $fistCountryRegionCode);
|
||||
$q->where(function ($q) use ($country, $region) {
|
||||
$q->where(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY, $country)
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_REGION, $region);
|
||||
});
|
||||
foreach ($this->regions as $countryRegioncode) {
|
||||
// regions must be in a form countryCode-regionCode
|
||||
[$country, $region] = explode('-', $countryRegioncode);
|
||||
$q->orWhere(function ($q) use ($country, $region) {
|
||||
$q->where(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY, $country)
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_REGION, $region);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->cities)) {
|
||||
// get first city (so that we can use where and then orWhere query)
|
||||
$fistCountryRegionCity = array_shift($this->cities);
|
||||
// cities must be in a form countryCode-regionCode-cityName
|
||||
[$country, $region, $city] = explode('-', $fistCountryRegionCity);
|
||||
$q->where(function ($q) use ($country, $region, $city) {
|
||||
$q->where(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY, $country)
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_REGION, $region)
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_CITY, 'like', $city . '%');
|
||||
});
|
||||
foreach ($this->cities as $countryRegionCity) {
|
||||
// cities must be in a form countryCode-regionCode-cityName
|
||||
[$country, $region, $city] = explode('-', $countryRegionCity);
|
||||
$q->orWhere(function ($q) use ($country, $region, $city) {
|
||||
$q->where(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY, $country)
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_REGION, $region)
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_CITY, 'like', $city . '%');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$q->whereBetween(StatisticsHelper::STATISTICS_DIMENSION_MONTH, [date_format(date_create($this->dateStart), 'Ym'), date_format(date_create($this->dateEnd), 'Ym')]);
|
||||
|
||||
if (!empty($this->pkpSectionIds)) {
|
||||
$sectionColumn = 'p.' . $this->getSectionColumn();
|
||||
$sectionSubmissionIds = DB::table('publications as p')->select('p.submission_id')->distinct()
|
||||
->from('publications as p')
|
||||
->where('p.status', Submission::STATUS_PUBLISHED)
|
||||
->whereIn($sectionColumn, $this->pkpSectionIds);
|
||||
$q->joinSub($sectionSubmissionIds, 'ss', function ($join) {
|
||||
$join->on('metrics_submission_geo_monthly.' . StatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID, '=', 'ss.submission_id');
|
||||
});
|
||||
}
|
||||
|
||||
$this->_getAppSpecificQuery($q);
|
||||
|
||||
if ($this->limit > 0) {
|
||||
$q->limit($this->limit);
|
||||
if ($this->offset > 0) {
|
||||
$q->offset($this->offset);
|
||||
}
|
||||
}
|
||||
|
||||
Hook::call('StatsGeo::queryObject', [&$q, $this]);
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do usage stats data already exist for the given month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function monthExists(string $month): bool
|
||||
{
|
||||
return DB::table('metrics_submission_geo_monthly')
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_MONTH, $month)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete daily usage metrics for a month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function deleteDailyMetrics(string $month): void
|
||||
{
|
||||
// Construct the SQL part depending on the DB
|
||||
$monthFormatSql = "DATE_FORMAT(date, '%Y%m')";
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$monthFormatSql = "to_char(date, 'YYYYMM')";
|
||||
}
|
||||
DB::table('metrics_submission_geo_daily')->where(DB::raw($monthFormatSql), '=', $month)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete monthly usage metrics for a month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function deleteMonthlyMetrics(string $month): void
|
||||
{
|
||||
DB::table('metrics_submission_geo_monthly')->where('month', $month)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate daily usage metrics by a month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function addMonthlyMetrics(string $month): void
|
||||
{
|
||||
// Construct the SQL part depending on the DB
|
||||
$monthFormatSql = "CAST(DATE_FORMAT(gd.date, '%Y%m') AS UNSIGNED)";
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$monthFormatSql = "to_char(gd.date, 'YYYYMM')::integer";
|
||||
}
|
||||
$selectSubmissionGeoDaily = DB::table('metrics_submission_geo_daily as gd')
|
||||
->select(DB::raw("gd.context_id, gd.submission_id, COALESCE(gd.country, ''), COALESCE(gd.region, ''), COALESCE(gd.city, ''), {$monthFormatSql} as gdmonth, SUM(gd.metric), SUM(gd.metric_unique)"))
|
||||
->whereRaw("{$monthFormatSql} = ?", [$month])
|
||||
->groupBy(DB::raw('gd.context_id, gd.submission_id, gd.country, gd.region, gd.city, gdmonth'));
|
||||
DB::table('metrics_submission_geo_monthly')->insertUsing(['context_id', 'submission_id', 'country', 'region', 'city', 'month', 'metric', 'metric_unique'], $selectSubmissionGeoDaily);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/queryBuilders/PKPStatsPublicationQueryBuilder.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 PKPStatsPublicationQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Helper class to construct a query to fetch stats records from the
|
||||
* metrics_submission table.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use APP\core\Application;
|
||||
use APP\submission\Submission;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\plugins\Hook;
|
||||
use PKP\statistics\PKPStatisticsHelper;
|
||||
|
||||
abstract class PKPStatsPublicationQueryBuilder extends PKPStatsQueryBuilder
|
||||
{
|
||||
/**
|
||||
*Include records for one of these object types:
|
||||
* Application::ASSOC_TYPE_SUBMISSION, Application::ASSOC_TYPE_SUBMISSION_FILE, Application::ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER
|
||||
*/
|
||||
protected array $assocTypes = [];
|
||||
|
||||
/** Include records for these file types: PKPStatisticsHelper::STATISTICS_FILE_TYPE_* */
|
||||
protected array $fileTypes = [];
|
||||
|
||||
/** Include records for these sections/series */
|
||||
protected array $pkpSectionIds = [];
|
||||
|
||||
/** Include records for these submissions */
|
||||
protected array $submissionIds = [];
|
||||
|
||||
/** Include records for these representations (galley or publication format) */
|
||||
protected array $representationIds = [];
|
||||
|
||||
/** Include records for these submission files */
|
||||
protected array $submissionFileIds = [];
|
||||
|
||||
/** Application specific name of the section column */
|
||||
abstract public function getSectionColumn(): string;
|
||||
|
||||
/**
|
||||
* Set the sections/series to get records for
|
||||
*/
|
||||
public function filterByPKPSections(array $pkpSectionIds): self
|
||||
{
|
||||
$this->pkpSectionIds = $pkpSectionIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the submissions to get records for
|
||||
*/
|
||||
public function filterBySubmissions(array $submissionIds): self
|
||||
{
|
||||
$this->submissionIds = $submissionIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the representations to get records for
|
||||
*/
|
||||
public function filterByRepresentations(array $representationIds): self
|
||||
{
|
||||
$this->representationIds = $representationIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the files to get records for
|
||||
*/
|
||||
public function filterBySubmissionFiles(array $submissionFileIds): self
|
||||
{
|
||||
$this->submissionFileIds = $submissionFileIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the assocTypes to get records for
|
||||
*/
|
||||
public function filterByAssocTypes(array $assocTypes): self
|
||||
{
|
||||
$this->assocTypes = $assocTypes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the galley file type to get records for
|
||||
*/
|
||||
public function filterByFileTypes(array $fileTypes): self
|
||||
{
|
||||
$this->fileTypes = $fileTypes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get submission IDs
|
||||
*/
|
||||
public function getSubmissionIds(): Builder
|
||||
{
|
||||
return $this->_getObject()
|
||||
->select(['metrics_submission.' . PKPStatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID])
|
||||
->distinct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::getSum()
|
||||
*/
|
||||
public function getSum(array $groupBy = []): Builder
|
||||
{
|
||||
$groupBy = array_map(function ($column) {
|
||||
return $column == PKPStatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID ? 'metrics_submission.' . $column : $column;
|
||||
}, $groupBy);
|
||||
return parent::getSum($groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider/add application specific queries
|
||||
*/
|
||||
protected function _getAppSpecificQuery(Builder &$q): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::_getObject()
|
||||
*/
|
||||
protected function _getObject(): Builder
|
||||
{
|
||||
$q = DB::table('metrics_submission');
|
||||
|
||||
if (!empty($this->contextIds)) {
|
||||
$q->whereIn(PKPStatisticsHelper::STATISTICS_DIMENSION_CONTEXT_ID, $this->contextIds);
|
||||
}
|
||||
|
||||
if (!empty($this->submissionIds)) {
|
||||
$q->whereIn('metrics_submission.' . PKPStatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID, $this->submissionIds);
|
||||
}
|
||||
|
||||
if (!empty($this->assocTypes)) {
|
||||
$q->whereIn(PKPStatisticsHelper::STATISTICS_DIMENSION_ASSOC_TYPE, $this->assocTypes);
|
||||
}
|
||||
|
||||
if (!empty($this->fileTypes)) {
|
||||
$q->whereIn(PKPStatisticsHelper::STATISTICS_DIMENSION_FILE_TYPE, $this->fileTypes);
|
||||
}
|
||||
|
||||
if (!empty($this->representationIds)) {
|
||||
$q->whereIn(PKPStatisticsHelper::STATISTICS_DIMENSION_REPRESENTATION_ID, $this->representationIds);
|
||||
}
|
||||
|
||||
if (!empty($this->submissionFileIds)) {
|
||||
$q->whereIn(PKPStatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_FILE_ID, $this->submissionFileIds);
|
||||
}
|
||||
|
||||
$q->whereBetween(PKPStatisticsHelper::STATISTICS_DIMENSION_DATE, [$this->dateStart, $this->dateEnd]);
|
||||
|
||||
if (!empty($this->pkpSectionIds)) {
|
||||
$sectionColumn = 'p.' . $this->getSectionColumn();
|
||||
$sectionSubmissionIds = DB::table('publications as p')->select('p.submission_id')->distinct()
|
||||
->from('publications as p')
|
||||
->where('p.status', Submission::STATUS_PUBLISHED)
|
||||
->whereIn($sectionColumn, $this->pkpSectionIds);
|
||||
$q->joinSub($sectionSubmissionIds, 'ss', function ($join) {
|
||||
$join->on('metrics_submission.' . PKPStatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID, '=', 'ss.submission_id');
|
||||
});
|
||||
}
|
||||
|
||||
$this->_getAppSpecificQuery($q);
|
||||
|
||||
if ($this->limit > 0) {
|
||||
$q->limit($this->limit);
|
||||
if ($this->offset > 0) {
|
||||
$q->offset($this->offset);
|
||||
}
|
||||
}
|
||||
|
||||
Hook::call('StatsPublication::queryObject', [&$q, $this]);
|
||||
|
||||
return $q;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/queryBuilders/PKPStatsQueryBuilder.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class PKPStatsQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Base class for statistics query builders.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\config\Config;
|
||||
use PKP\statistics\PKPStatisticsHelper;
|
||||
|
||||
abstract class PKPStatsQueryBuilder
|
||||
{
|
||||
/** Include records for these contexts */
|
||||
protected array $contextIds = [];
|
||||
|
||||
/** Include records from this date or before. Default: yesterday's date */
|
||||
protected string $dateEnd;
|
||||
|
||||
/** Include records from this date or after. Default: PKPStatisticsHelper::STATISTICS_EARLIEST_DATE */
|
||||
protected string $dateStart;
|
||||
|
||||
/** The count of records to return */
|
||||
protected int $limit = 0;
|
||||
|
||||
/** The offset of records to return */
|
||||
protected int $offset = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Set the contexts to get records for
|
||||
*/
|
||||
public function filterByContexts(array $contextIds): self
|
||||
{
|
||||
$this->contextIds = $contextIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the date before which to get records
|
||||
*
|
||||
* @param string $dateEnd YYYY-MM-DD
|
||||
*
|
||||
*/
|
||||
public function before(string $dateEnd): self
|
||||
{
|
||||
$this->dateEnd = $dateEnd;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the date after which to get records
|
||||
*
|
||||
* @param string $dateStart YYYY-MM-DD
|
||||
*
|
||||
*/
|
||||
public function after(string $dateStart): self
|
||||
{
|
||||
$this->dateStart = $dateStart;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the count of records to return
|
||||
*/
|
||||
public function limit(int $limit): self
|
||||
{
|
||||
$this->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the offset of records to return
|
||||
*/
|
||||
public function offset(int $offset): self
|
||||
{
|
||||
$this->offset = $offset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sum of all matching records
|
||||
*
|
||||
* Use this method to get the total X views. Pass a
|
||||
* $groupBy argument to get the total X views for each
|
||||
* object, grouped by one or more columns.
|
||||
*
|
||||
* @param array $groupBy One or more columns to group by
|
||||
*
|
||||
*/
|
||||
public function getSum(array $groupBy = []): Builder
|
||||
{
|
||||
$selectColumns = $groupBy;
|
||||
$selectColumns = $this->getSelectColumns($selectColumns);
|
||||
|
||||
$q = $this->_getObject();
|
||||
// Build the select and group by clauses.
|
||||
if (!empty($selectColumns)) {
|
||||
$q->select($selectColumns);
|
||||
if (!empty($groupBy)) {
|
||||
$q->groupBy($groupBy);
|
||||
}
|
||||
}
|
||||
$q->addSelect(DB::raw('SUM(metric) AS metric'));
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a query object based on the configured conditions.
|
||||
*
|
||||
* Public methods should call this method to set up the query
|
||||
* object and apply any additional selection, grouping and
|
||||
* ordering conditions.
|
||||
*/
|
||||
abstract protected function _getObject(): Builder;
|
||||
|
||||
/**
|
||||
* Get appropriate SQL code for columns in the select part of the query
|
||||
*/
|
||||
protected function getSelectColumns(array $selectColumns): array
|
||||
{
|
||||
if (!in_array(PKPStatisticsHelper::STATISTICS_DIMENSION_YEAR, $selectColumns)
|
||||
&& !in_array(PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH, $selectColumns)
|
||||
&& !in_array(PKPStatisticsHelper::STATISTICS_DIMENSION_DAY, $selectColumns)) {
|
||||
return $selectColumns;
|
||||
}
|
||||
foreach ($selectColumns as $i => $selectColumn) {
|
||||
if ($selectColumn == PKPStatisticsHelper::STATISTICS_DIMENSION_YEAR) {
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
// date_trunc: Values of type date are cast automatically to timestamp. So cast them back to date.
|
||||
$selectColumns[$i] = DB::raw("date_trunc('year', date)::timestamp::date AS year");
|
||||
} else {
|
||||
$selectColumns[$i] = DB::raw("date_format(date, '%Y-01-01') AS year");
|
||||
}
|
||||
break;
|
||||
} elseif ($selectColumn == PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH) {
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
// date_trunc: Values of type date are cast automatically to timestamp. So cast them back to date.
|
||||
$selectColumns[$i] = DB::raw("date_trunc('month', date)::timestamp::date AS month");
|
||||
} else {
|
||||
$selectColumns[$i] = DB::raw("date_format(date, '%Y-%m-01') AS month");
|
||||
}
|
||||
break;
|
||||
} elseif ($selectColumn == PKPStatisticsHelper::STATISTICS_DIMENSION_DAY) {
|
||||
$selectColumns[$i] = DB::raw('date AS day');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $selectColumns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/queryBuilders/PKPStatsSushiQueryBuilder.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 PKPStatsSushiQueryBuilder
|
||||
*
|
||||
* @ingroup query_builders
|
||||
*
|
||||
* @brief Helper class to construct a query to fetch COUNTER stats records from the
|
||||
* metrics_counter_submission_monthly or metrics_counter_submission_institution_monthly table.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders;
|
||||
|
||||
use APP\statistics\StatisticsHelper;
|
||||
use APP\submission\Submission;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PKP\config\Config;
|
||||
use PKP\plugins\Hook;
|
||||
|
||||
class PKPStatsSushiQueryBuilder extends PKPStatsQueryBuilder
|
||||
{
|
||||
/** Include records for the submissions that have these years of publications (YOP) */
|
||||
protected array $yearsOfPublication = [];
|
||||
|
||||
/**Include records for these submissions */
|
||||
protected array $submissionIds = [];
|
||||
|
||||
/**Include records for this institution */
|
||||
protected int $institutionId = 0;
|
||||
|
||||
/**
|
||||
* Set the year of publication (YOP) of submissions to get records for
|
||||
*/
|
||||
public function filterByYOP(array $yearsOfPublication): self
|
||||
{
|
||||
$this->yearsOfPublication = $yearsOfPublication;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the submissions to get records for
|
||||
*/
|
||||
public function filterBySubmissions(array $submissionIds): self
|
||||
{
|
||||
$this->submissionIds = $submissionIds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the institution to get records for
|
||||
*/
|
||||
public function filterByInstitution(int $institutionId): self
|
||||
{
|
||||
$this->institutionId = $institutionId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::getSum()
|
||||
*/
|
||||
public function getSum(array $groupBy = []): Builder
|
||||
{
|
||||
$selectColumns = $groupBy;
|
||||
$q = $this->_getObject();
|
||||
// consider YOP
|
||||
if (in_array('YOP', $selectColumns)) {
|
||||
// left join the table publications, if the filter is not set i.e. the left join is not considered yet in _getObject()
|
||||
if (empty($this->yearsOfPublication)) {
|
||||
$q->leftJoin('publications as p', function ($q) {
|
||||
$q->on('p.submission_id', '=', 'm.submission_id')
|
||||
->whereIn('p.publication_id', function ($q) {
|
||||
$q->selectRaw('MIN(p2.publication_id)')
|
||||
->from('publications as p2')
|
||||
->where('p2.status', Submission::STATUS_PUBLISHED)
|
||||
->where('p2.submission_id', '=', DB::raw('m.submission_id'));
|
||||
});
|
||||
});
|
||||
}
|
||||
foreach ($selectColumns as $i => $selectColumn) {
|
||||
if ($selectColumn == 'YOP') {
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$selectColumns[$i] = DB::raw('EXTRACT(YEAR FROM p.date_published) as "YOP"');
|
||||
} else {
|
||||
$selectColumns[$i] = DB::raw('YEAR(STR_TO_DATE(p.date_published, "%Y-%m-%d")) as YOP');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the select and group by clauses.
|
||||
if (!empty($selectColumns)) {
|
||||
$q->select($selectColumns);
|
||||
if (!empty($groupBy)) {
|
||||
$q->groupBy($groupBy);
|
||||
}
|
||||
}
|
||||
$counterMetricsColumns = StatisticsHelper::getCounterMetricsColumns();
|
||||
foreach ($counterMetricsColumns as $counterMetricsColumn) {
|
||||
$q->addSelect(DB::raw("SUM({$counterMetricsColumn}) AS {$counterMetricsColumn}"));
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* @copydoc PKPStatsQueryBuilder::_getObject()
|
||||
*/
|
||||
protected function _getObject(): Builder
|
||||
{
|
||||
if ($this->institutionId === 0) {
|
||||
$q = DB::table('metrics_counter_submission_monthly as m');
|
||||
} else {
|
||||
$q = DB::table('metrics_counter_submission_institution_monthly as m');
|
||||
}
|
||||
|
||||
if (!empty($this->yearsOfPublication)) {
|
||||
$q->leftJoin('publications as p', function ($q) {
|
||||
$q->on('p.submission_id', '=', 'm.submission_id')
|
||||
->whereIn('p.publication_id', function ($q) {
|
||||
$q->selectRaw('MIN(p2.publication_id)')
|
||||
->from('publications as p2')
|
||||
->where('p2.status', Submission::STATUS_PUBLISHED)
|
||||
->where('p2.submission_id', '=', DB::raw('m.submission_id'));
|
||||
});
|
||||
});
|
||||
foreach ($this->yearsOfPublication as $yop) {
|
||||
if (preg_match('/\d{4}/', $yop)) {
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$q->where(DB::raw('EXTRACT(YEAR FROM p.date_published)'), '=', $yop);
|
||||
} else {
|
||||
$q->where(DB::raw('YEAR(STR_TO_DATE(p.date_published, "%Y-%m-%d"))'), '=', $yop);
|
||||
}
|
||||
} elseif (preg_match('/\d{4}-\d{4}/', $yop)) {
|
||||
$years = explode('-', $yop);
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$q->whereBetween(DB::raw('EXTRACT(YEAR FROM p.date_published)'), $years);
|
||||
} else {
|
||||
$q->whereBetween(DB::raw('YEAR(STR_TO_DATE(p.date_published, "%Y-%m-%d"))'), $years);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->contextIds)) {
|
||||
$q->whereIn('m.' . StatisticsHelper::STATISTICS_DIMENSION_CONTEXT_ID, $this->contextIds);
|
||||
}
|
||||
|
||||
if (!empty($this->submissionIds)) {
|
||||
$q->whereIn('m.' . StatisticsHelper::STATISTICS_DIMENSION_SUBMISSION_ID, $this->submissionIds);
|
||||
}
|
||||
|
||||
$q->whereBetween('m.' . StatisticsHelper::STATISTICS_DIMENSION_MONTH, [date_format(date_create($this->dateStart), 'Ym'), date_format(date_create($this->dateEnd), 'Ym')]);
|
||||
|
||||
Hook::call('StatsSushi::queryObject', [&$q, $this]);
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do usage stats data already exist for the given month
|
||||
* Consider only the table metrics_counter_submission_monthly, because
|
||||
* it always contains data, while metrics_counter_submission_institution_monthly
|
||||
* could not contain data.
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function monthExists(string $month): bool
|
||||
{
|
||||
return DB::table('metrics_counter_submission_monthly as m')
|
||||
->where(StatisticsHelper::STATISTICS_DIMENSION_MONTH, $month)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete daily usage stats for a month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function deleteDailyMetrics(string $month): void
|
||||
{
|
||||
// Construct the SQL part depending on the DB
|
||||
$monthFormatSql = "DATE_FORMAT(date, '%Y%m')";
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$monthFormatSql = "to_char(date, 'YYYYMM')";
|
||||
}
|
||||
DB::table('metrics_counter_submission_daily')->where(DB::raw($monthFormatSql), '=', $month)->delete();
|
||||
DB::table('metrics_counter_submission_institution_daily')->where(DB::raw($monthFormatSql), '=', $month)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete monthly usage metrics for a month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function deleteMonthlyMetrics(string $month): void
|
||||
{
|
||||
DB::table('metrics_counter_submission_monthly')->where('month', $month)->delete();
|
||||
DB::table('metrics_counter_submission_institution_monthly')->where('month', $month)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate daily usage metrics by a month
|
||||
*
|
||||
* @param string $month Month in the form YYYYMM
|
||||
*/
|
||||
public function addMonthlyMetrics(string $month): void
|
||||
{
|
||||
// Construct the SQL part depending on the DB
|
||||
$monthFormatSql = "CAST(DATE_FORMAT(csd.date, '%Y%m') AS UNSIGNED)";
|
||||
if (substr(Config::getVar('database', 'driver'), 0, strlen('postgres')) === 'postgres') {
|
||||
$monthFormatSql = "to_char(csd.date, 'YYYYMM')::integer";
|
||||
}
|
||||
// Get the application specific metrics columns
|
||||
$counterMetricsColumns = StatisticsHelper::getCounterMetricsColumns();
|
||||
// SQL part for the select sub-statement creates the SUM for each metrics column, and then connects them with ','
|
||||
$selectSql = implode(', ', array_map(fn ($value): string => 'SUM(csd.' . $value . ')', $counterMetricsColumns));
|
||||
|
||||
$selectSubmissionDaily = DB::table('metrics_counter_submission_daily as csd')
|
||||
->select(DB::raw("csd.context_id, csd.submission_id, {$monthFormatSql} as csdmonth, {$selectSql}"))
|
||||
->whereRaw("{$monthFormatSql} = ?", [$month])
|
||||
->groupBy(DB::raw('csd.context_id, csd.submission_id, csdmonth'));
|
||||
DB::table('metrics_counter_submission_monthly')->insertUsing(array_merge(['context_id', 'submission_id', 'month'], $counterMetricsColumns), $selectSubmissionDaily);
|
||||
|
||||
$selectSubmissionInstitutionDaily = DB::table('metrics_counter_submission_institution_daily as csd')
|
||||
->select(DB::raw("csd.context_id, csd.submission_id, csd.institution_id, {$monthFormatSql} as csdmonth, {$selectSql}"))
|
||||
->whereRaw("{$monthFormatSql} = ?", [$month])
|
||||
->groupBy(DB::raw('csd.context_id, csd.submission_id, csd.institution_id, csdmonth'));
|
||||
DB::table('metrics_counter_submission_institution_monthly')->insertUsing(array_merge(['context_id', 'submission_id', 'institution_id', 'month'], $counterMetricsColumns), $selectSubmissionInstitutionDaily);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/services/queryBuilders/interfaces/EntityQueryBuilderInterface.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class EntityQueryBuilderInterface
|
||||
*
|
||||
* @ingroup services_query_builders
|
||||
*
|
||||
* @brief An interface that defines required methods for
|
||||
* a QueryBuilder that retrieves one of the application's
|
||||
* entities.
|
||||
*/
|
||||
|
||||
namespace PKP\services\queryBuilders\interfaces;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
interface EntityQueryBuilderInterface
|
||||
{
|
||||
/**
|
||||
* Get a count of the number of rows that match the select
|
||||
* conditions configured in this query builder.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCount();
|
||||
|
||||
/**
|
||||
* Get a list of ids that match the select conditions
|
||||
* configured in this query builder.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getIds();
|
||||
|
||||
/**
|
||||
* Get a query builder with the applied select, where and
|
||||
* join clauses based on builder's configuration
|
||||
*
|
||||
* This returns an instance of Laravel's query builder.
|
||||
*
|
||||
* Call the `get` method on a query builder to return an array
|
||||
* of matching rows.
|
||||
*
|
||||
* ```php
|
||||
* $qb = new \PKP\services\queryBuilders\PublicationQueryBuilder();
|
||||
* $result = $qb
|
||||
* ->filterByContextIds(1)
|
||||
* ->getQuery()
|
||||
* ->get();
|
||||
* ```
|
||||
*
|
||||
* Or use the query builder to retrieve objects from a DAO.
|
||||
* This example retrieves the first 20 matching Publications.
|
||||
*
|
||||
* ```php
|
||||
* $qo = $qb
|
||||
* ->filterByContextIds(1)
|
||||
* ->getQuery();
|
||||
* $result = DAORegistry::getDAO('ReviewRoundDAO')->retrieveRange(
|
||||
* $qo->toSql(),
|
||||
* $qo->getBindings(),
|
||||
* new DBResultRange(20, null, 0);
|
||||
* );
|
||||
* $queryResults = new DAOResultFactory($result, $reviewRoundDao, '_fromRow');
|
||||
* $iteratorOfObjects = $queryResults->toIterator();
|
||||
* ```
|
||||
*
|
||||
* Laravel's other query builder methods, such as `first`
|
||||
* and `pluck`, can also be used.
|
||||
*
|
||||
* ```
|
||||
* $qb = new \PKP\services\queryBuilders\PublicationQueryBuilder();
|
||||
* $result = $qb
|
||||
* ->filterByContextIds(1)
|
||||
* ->getQuery()
|
||||
* ->first();
|
||||
* ```
|
||||
*
|
||||
* See: https://laravel.com/docs/5.5/queries
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function getQuery();
|
||||
}
|
||||
Reference in New Issue
Block a user