[ Mini Kiebo ]
Server: Windows NT DESKTOP-5B8S0D4 6.2 build 9200 (Windows 8 Professional Edition) i586
Path:
D:
/
Backup
/
05122024
/
htdocs
/
jurnal-kesmas
/
v1
/
lib
/
pkp
/
classes
/
submission
/
[
Home
]
File: Collector.php
<?php /** * @file classes/submission/Collector.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 Collector * * @brief A helper class to configure a Query Builder to get a collection of submissions */ namespace PKP\submission; use APP\core\Application; use APP\facades\Repo; use APP\submission\Collector as AppCollector; use APP\submission\Submission; use Exception; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\LazyCollection; use PKP\core\Core; use PKP\core\interfaces\CollectorInterface; use PKP\facades\Locale; use PKP\identity\Identity; use PKP\plugins\Hook; use PKP\search\SubmissionSearch; use PKP\security\Role; use PKP\submission\reviewRound\ReviewRound; /** * @template T of Submission */ abstract class Collector implements CollectorInterface { public const ORDERBY_DATE_PUBLISHED = 'datePublished'; public const ORDERBY_DATE_SUBMITTED = 'dateSubmitted'; public const ORDERBY_LAST_ACTIVITY = 'lastActivity'; public const ORDERBY_LAST_MODIFIED = 'lastModified'; public const ORDERBY_SEQUENCE = 'sequence'; public const ORDERBY_TITLE = 'title'; public const ORDERBY_SEARCH_RANKING = 'ranking'; public const ORDER_DIR_ASC = 'ASC'; public const ORDER_DIR_DESC = 'DESC'; public const UNASSIGNED = -1; public DAO $dao; public ?array $categoryIds = null; public ?array $contextIds = null; public ?int $count = null; public ?int $daysInactive = null; public bool $isIncomplete = false; public bool $isOverdue = false; public ?int $offset = null; public string $orderBy = self::ORDERBY_DATE_SUBMITTED; public string $orderDirection = 'DESC'; public ?string $searchPhrase = null; public ?int $maxSearchKeywords = null; public ?array $statuses = null; public ?array $stageIds = null; public ?array $doiStatuses = null; public ?bool $hasDois = null; public ?array $excludeIds = null; /** @var array Which DOI types should be considered when checking if a submission has DOIs set */ public array $enabledDoiTypes = []; /** @var array|int */ public $assignedTo = null; public function __construct(DAO $dao) { $this->dao = $dao; } public function getCount(): int { return $this->dao->getCount($this); } /** * @return Collection<int,int> */ public function getIds(): Collection { return $this->dao->getIds($this); } /** * @copydoc DAO::getMany() * @return LazyCollection<int,T> */ public function getMany(): LazyCollection { return $this->dao->getMany($this); } /** * Limit results to submissions in these contexts */ public function filterByContextIds(?array $contextIds): AppCollector { $this->contextIds = $contextIds; return $this; } /** * Limit results by submissions assigned to these categories */ public function filterByCategoryIds(?array $categoryIds): AppCollector { $this->categoryIds = $categoryIds; return $this; } /** * Limit results to submissions that contain any pub objects (e.g. publication and galley) with these statuses * * @param array|null $statuses One or more of DOI::STATUS_* constants * */ public function filterByDoiStatuses(?array $statuses): AppCollector { $this->doiStatuses = $statuses; return $this; } /** * Limit results to submissions that do/don't have any DOIs assign to their sub objects * * @param array|null $enabledDoiTypes TYPE_* constants to consider when checking submission has DOIs */ public function filterByHasDois(?bool $hasDois, ?array $enabledDoiTypes = null): AppCollector { $this->hasDois = $hasDois; $this->enabledDoiTypes = $enabledDoiTypes === null ? [Repo::doi()::TYPE_PUBLICATION] : $enabledDoiTypes; return $this; } /** * Limit results by submissions with these statuses * * @see \PKP\submissions\PKPSubmission::STATUS_ */ public function filterByStatus(?array $statuses): AppCollector { $this->statuses = $statuses; return $this; } /** * Limit results by submissions in these workflow stage ids */ public function filterByStageIds(?array $stageIds): AppCollector { $this->stageIds = $stageIds; return $this; } /** * Limit results to incomplete submissions * * Submissions are incomplete when the author has begun to enter * details about their submission but not yet submitted it. */ public function filterByIncomplete(bool $isIncomplete): AppCollector { $this->isIncomplete = $isIncomplete; return $this; } /** * Limit results to submissions with overdue tasks */ public function filterByOverdue(bool $isOverdue): AppCollector { $this->isOverdue = $isOverdue; return $this; } /** * Limit results to submission with no activity for X days */ public function filterByDaysInactive(?int $daysInactive): AppCollector { $this->daysInactive = $daysInactive; return $this; } /** * Limit results to submissions assigned to these users * * @param int|array $assignedTo An array of user IDs * or self::UNASSIGNED to get unassigned submissions */ public function assignedTo($assignedTo): AppCollector { $this->assignedTo = $assignedTo; return $this; } /** * Limit results to submissions matching this search query */ public function searchPhrase(?string $phrase, ?int $maxSearchKeywords = null): AppCollector { $this->searchPhrase = $phrase; $this->maxSearchKeywords = $maxSearchKeywords; return $this; } /** * Ensure the given submission IDs are not included */ public function excludeIds(?array $ids): AppCollector { $this->excludeIds = $ids; return $this; } /** * Limit the number of objects retrieved */ public function limit(?int $count): AppCollector { $this->count = $count; return $this; } /** * Offset the number of objects retrieved, for example to * retrieve the second page of contents */ public function offset(?int $offset): AppCollector { $this->offset = $offset; return $this; } /** * Order the results * * The following column values are supported: * * - lastModified * - dateLastActivity * - title * - seq (sequence) * - DAO::ORDERBY_DATE_PUBLISHED * * Results are ordered by the date submitted by default. * * @param string $sorter One of the self::ORDERBY_ constants * @param string $direction One of the self::ORDER_DIR_ constants */ public function orderBy(string $sorter, string $direction = self::ORDER_DIR_DESC): AppCollector { $this->orderBy = $sorter; $this->orderDirection = $direction; return $this; } /** * Add APP-specific filtering methods for submission sub objects DOI statuses * */ abstract protected function addDoiStatusFilterToQuery(Builder $q); /** * Add APP-specific filtering methods for checking if submission sub objects have DOIs assigned */ abstract protected function addHasDoisFilterToQuery(Builder $q); /** * @copydoc CollectorInterface::getQueryBuilder() */ public function getQueryBuilder(): Builder { $q = DB::table('submissions AS s') ->leftJoin('publications AS po', 's.current_publication_id', '=', 'po.publication_id') ->select(['s.*']); // Never permit a query without a context_id unless the CONTEXT_ID_ALL wildcard // has been set explicitly. if (!isset($this->contextIds)) { throw new Exception('Submissions can not be retrieved without a context id. Pass the CONTEXT_ID_ALL wildcard to get submissions from any context.'); } elseif (!in_array(Application::CONTEXT_ID_ALL, $this->contextIds)) { $q->whereIn('s.context_id', $this->contextIds); } // Prepare keywords (allows short and numeric words) $keywords = collect(Application::getSubmissionSearchIndex()->filterKeywords($this->searchPhrase, false, true, true)) ->unique() ->take($this->maxSearchKeywords ?? PHP_INT_MAX); // Setup the order by switch ($this->orderBy) { case self::ORDERBY_DATE_PUBLISHED: $q->addSelect(['po.date_published']); $q->orderBy('po.date_published', $this->orderDirection); break; case self::ORDERBY_LAST_ACTIVITY: $q->orderBy('s.date_last_activity', $this->orderDirection); break; case self::ORDERBY_LAST_MODIFIED: $q->orderBy('s.last_modified', $this->orderDirection); break; case self::ORDERBY_SEQUENCE: $q->addSelect(['po.seq']); $q->orderBy('po.seq', $this->orderDirection); break; case self::ORDERBY_TITLE: $locale = Locale::getLocale(); $q->leftJoin('publications as publication_tlp', 's.current_publication_id', '=', 'publication_tlp.publication_id') ->leftJoin('publication_settings as publication_tlps', fn (JoinClause $join) => $join->on('publication_tlp.publication_id', '=', 'publication_tlps.publication_id') ->where('publication_tlps.setting_name', '=', 'title') ->where('publication_tlps.setting_value', '!=', '') ->where('publication_tlps.locale', '=', $locale) ); $q->leftJoin('publications as publication_tlpl', 's.current_publication_id', '=', 'publication_tlpl.publication_id') ->leftJoin('publication_settings as publication_tlpsl', fn (JoinClause $join) => $join->on('publication_tlp.publication_id', '=', 'publication_tlpsl.publication_id') ->on('publication_tlpsl.locale', '=', 's.locale') ->where('publication_tlpsl.setting_name', '=', 'title') ); $coalesceTitles = 'COALESCE(publication_tlps.setting_value, publication_tlpsl.setting_value)'; $q->addSelect([DB::raw($coalesceTitles)]); $q->orderBy(DB::raw($coalesceTitles), $this->orderDirection); break; case self::ORDERBY_SEARCH_RANKING: if (!$keywords->count()) { $q->orderBy('s.date_submitted', $this->orderDirection); break; } // Retrieves the number of matches for all keywords $orderByMatchCount = DB::table('submission_search_objects', 'sso') ->join('submission_search_object_keywords AS ssok', 'ssok.object_id', '=', 'sso.object_id') ->join('submission_search_keyword_list AS sskl', 'sskl.keyword_id', '=', 'ssok.keyword_id') ->where(fn (Builder $q) => $keywords->map(fn (string $keyword) => $q ->orWhere('sskl.keyword_text', '=', DB::raw('LOWER(?)')) ->addBinding($keyword) ) ) ->whereColumn('s.submission_id', '=', 'sso.submission_id') ->selectRaw('COUNT(0)'); // Retrieves the number of distinct matched keywords $orderByDistinctKeyword = (clone $orderByMatchCount)->select(DB::raw('COUNT(DISTINCT sskl.keyword_id)')); $q->orderBy($orderByDistinctKeyword, $this->orderDirection) ->orderBy($orderByMatchCount, $this->orderDirection); break; case self::ORDERBY_DATE_SUBMITTED: default: $q->orderBy('s.date_submitted', $this->orderDirection); break; } if (isset($this->statuses)) { $q->whereIn('s.status', $this->statuses); } if (isset($this->stageIds)) { $q->whereIn('s.stage_id', $this->stageIds); } if ($this->isIncomplete) { $q->where('s.submission_progress', '<>', ''); } if (isset($this->daysInactive)) { $q->where('s.date_last_activity', '<', Core::getCurrentDate(strtotime('-' . $this->daysInactive . ' days'))); } if ($this->isOverdue) { $q->leftJoin('review_assignments as raod', 'raod.submission_id', '=', 's.submission_id') ->leftJoin('review_rounds as rr', fn (Builder $table) => $table->on('rr.submission_id', '=', 's.submission_id') ->on('raod.review_round_id', '=', 'rr.review_round_id') ); // Only get overdue assignments on active review rounds $q->whereNotIn('rr.status', [ ReviewRound::REVIEW_ROUND_STATUS_RESUBMIT_FOR_REVIEW, ReviewRound::REVIEW_ROUND_STATUS_SENT_TO_EXTERNAL, ReviewRound::REVIEW_ROUND_STATUS_ACCEPTED, ReviewRound::REVIEW_ROUND_STATUS_DECLINED, ]); $q->where(fn (Builder $q) => $q->where('raod.declined', '<>', 1) ->where('raod.cancelled', '<>', 1) ->where(fn (Builder $q) => $q->where('raod.date_due', '<', Core::getCurrentDate(strtotime('tomorrow'))) ->whereNull('raod.date_completed') ) ->orWhere(fn (Builder $q) => $q->where('raod.date_response_due', '<', Core::getCurrentDate(strtotime('tomorrow'))) ->whereNull('raod.date_confirmed') ) ); } if (is_array($this->assignedTo)) { $q->whereIn('s.submission_id', fn (Builder $q) => $q->select('s.submission_id') ->from('submissions AS s') ->leftJoin('stage_assignments as sa', fn (Builder $q) => $q->on('s.submission_id', '=', 'sa.submission_id') ->whereIn('sa.user_id', $this->assignedTo) ) ->leftJoin('review_assignments as ra', fn (Builder $table) => $table->on('s.submission_id', '=', 'ra.submission_id') ->where('ra.declined', '=', (int) 0) ->where('ra.cancelled', '=', (int) 0) ->whereIn('ra.reviewer_id', $this->assignedTo) ) ->whereNotNull('sa.stage_assignment_id') ->orWhereNotNull('ra.review_id') ); } elseif ($this->assignedTo === self::UNASSIGNED) { $sub = DB::table('stage_assignments') ->select(DB::raw('count(stage_assignments.stage_assignment_id)')) ->leftJoin('user_groups', 'stage_assignments.user_group_id', '=', 'user_groups.user_group_id') ->where('stage_assignments.submission_id', '=', DB::raw('s.submission_id')) ->whereIn('user_groups.role_id', [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]); $q->whereNotNull('s.date_submitted') ->mergeBindings($sub) ->where(DB::raw('(' . $sub->toSql() . ')'), '=', '0'); } // Search phrase if ($keywords->count()) { $likePattern = DB::raw("CONCAT('%', LOWER(?), '%')"); if(!empty($this->assignedTo)) { // Holds a single random row to check whether we have any assignment $q->leftJoinSub(fn (Builder $q) => $q ->from('review_assignments', 'ra') ->whereIn('ra.reviewer_id', $this->assignedTo == self::UNASSIGNED ? [] : (array) $this->assignedTo) ->select(DB::raw('1 AS value')) ->limit(1), 'any_assignment', 'any_assignment.value', '=', DB::raw('1') ); } // Builds the filters $q->where(fn (Builder $q) => $keywords ->map(fn (string $keyword) => $q // Look for matches on the indexed data ->orWhereExists(fn (Builder $query) => $query ->from('submission_search_objects', 'sso') ->join('submission_search_object_keywords AS ssok', 'sso.object_id', '=', 'ssok.object_id') ->join('submission_search_keyword_list AS sskl', 'sskl.keyword_id', '=', 'ssok.keyword_id') ->where('sskl.keyword_text', '=', DB::raw('LOWER(?)'))->addBinding($keyword) ->whereColumn('s.submission_id', '=', 'sso.submission_id') // Don't permit reviewers to search on author names ->when(!empty($this->assignedTo), fn (Builder $q) => $q ->where(fn (Builder $q) => $q ->whereNull('any_assignment.value') ->orWhere('sso.type', '!=', SubmissionSearch::SUBMISSION_SEARCH_AUTHOR) ) ) ) // Search on the publication title ->orWhereIn('s.submission_id', fn (Builder $query) => $query ->select('p.submission_id')->from('publications AS p') ->join('publication_settings AS ps', 'p.publication_id', '=', 'ps.publication_id') ->where('ps.setting_name', '=', 'title') ->where(DB::raw('LOWER(ps.setting_value)'), 'LIKE', $likePattern) ->addBinding($keyword) ) // Search on the author name and ORCID ->orWhereIn('s.submission_id', fn (Builder $query) => $query ->select('p.submission_id') ->from('publications AS p') ->join('authors AS au', 'au.publication_id', '=', 'p.publication_id') ->join('author_settings AS aus', 'aus.author_id', '=', 'au.author_id') ->whereIn('aus.setting_name', [ Identity::IDENTITY_SETTING_GIVENNAME, Identity::IDENTITY_SETTING_FAMILYNAME, 'orcid' ]) // Don't permit reviewers to search on author names ->when(!empty($this->assignedTo), fn (Builder $q) => $q ->where(fn (Builder $q) => $q ->whereNull('any_assignment.value') ->orWhereNotIn('aus.setting_name', [ Identity::IDENTITY_SETTING_GIVENNAME, Identity::IDENTITY_SETTING_FAMILYNAME ]) ) ) ->where(DB::raw('LOWER(aus.setting_value)'), 'LIKE', $likePattern) ->addBinding($keyword) ) // Search for the exact submission ID ->when( ($numericWords = $keywords->filter(fn (string $keyword) => ctype_digit($keyword)))->count(), fn (Builder $query) => $query->orWhereIn('s.submission_id', $numericWords) ) ) ); } elseif (strlen($this->searchPhrase ?? '')) { // If there's search text, but no keywords could be extracted from it, force the query to return nothing $q->whereRaw('1 = 0'); } if (isset($this->categoryIds)) { $q->join('publication_categories as pc', 's.current_publication_id', '=', 'pc.publication_id') ->whereIn('pc.category_id', $this->categoryIds); } // Filter by any child pub object's DOI status $q->when($this->doiStatuses !== null, fn (Builder $q) => $this->addDoiStatusFilterToQuery($q)); // Filter by whether any child pub objects have DOIs assigned $q->when($this->hasDois !== null, fn (Builder $q) => $this->addHasDoisFilterToQuery($q)); // Filter out excluded submission IDs $q->when($this->excludeIds !== null, fn (Builder $q) => $q->whereNotIn('s.submission_id', $this->excludeIds)); // Limit and offset results for pagination if (isset($this->count)) { $q->limit($this->count); } if (isset($this->offset)) { $q->offset($this->offset); } // Add app-specific query statements Hook::call('Submission::Collector', [&$q, $this]); return $q; } }