Skip to content

Commit

Permalink
pkp#8887 DashboardView class and editorial submission views
Browse files Browse the repository at this point in the history
  • Loading branch information
Vitaliy-1 authored and ipula committed Dec 4, 2023
1 parent 792be39 commit 12c9dfb
Show file tree
Hide file tree
Showing 5 changed files with 605 additions and 180 deletions.
98 changes: 92 additions & 6 deletions api/v1/_submissions/PKPBackendSubmissionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\DashboardView;
use PKP\submission\PKPSubmission;

abstract class PKPBackendSubmissionsController extends PKPBaseController
{
Expand Down Expand Up @@ -112,6 +114,26 @@ public function getGroupRoutes(): void
Role::ROLE_ID_ASSISTANT,
]),
]);

Route::get('reviews', $this->assigned(...))
->name('_submission.reviews')
->middleware([
self::roleAuthorizer([
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_ASSISTANT,
])
]);

Route::get('viewsCount', $this->assigned(...))
->name('_submission.viewsCount')
->middleware([
self::roleAuthorizer([
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_ASSISTANT,
])
]);
}
}

Expand Down Expand Up @@ -243,21 +265,79 @@ public function assigned(Request $illuminateRequest): JsonResponse
}

/**
* Get submissions which need reviewer(s) to be assigned
* Get submission undergoing the review
*/
public function needsReviewers(SlimRequest $slimRequest, APIResponse $response, array $args)
public function reviews(SlimRequest $slimRequest, APIResponse $response, array $args)
{
$request = Application::get()->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$currentUser = $request->getUser();

$collector = $this->getSubmissionCollector($slimRequest->getQueryParams());
$submissions = $collector
->filterByContextIds($context->getId())
->filterByStageIds([WORKFLOW_STAGE_ID_INTERNAL_REVIEW, WORKFLOW_STAGE_ID_EXTERNAL_REVIEW])
->
$collector
->filterByContextIds([$context->getId()])
->filterByStatus([PKPSubmission::STATUS_QUEUED])
->filterByStageIds([WORKFLOW_STAGE_ID_INTERNAL_REVIEW, WORKFLOW_STAGE_ID_EXTERNAL_REVIEW]);

// limit results depending on a role

if (!$this->canAccessAllSubmissions()) {
$collector->assignedTo([$currentUser->getId()]);
}

$queryParams = $slimRequest->getQueryParams();
foreach ($queryParams as $param => $val) {
switch ($param) {
case 'needsReviewers':
$numReviewersPerSubmission = $context->getData('numReviewersPerSubmission');
$collector->filterByReviewersActive(range(0, (int) $numReviewersPerSubmission));
break;
case 'awaitingReviews':
$collector->filterByAwaitingReviews(true);
break;
case 'reviewsSubmitted':
$collector->filterByReviewsSubmitted(true);
break;
case 'reviewsOverdue':
$collector->filterByOverdue(true);
break;
}
}

$submissions = $collector->getMany();

$userGroups = Repo::userGroup()->getCollector()
->filterByContextIds([$context->getId()])
->getMany();

/** @var \PKP\submission\GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getByContextId($context->getId())->toArray();

return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::submission()->getSchemaMap()->mapManyToSubmissionsList($submissions, $userGroups, $genres)->values(),
], 200);
}

/**
* Get a number of the submissions for each view
*/
public function viewsCount(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = Application::get()->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$currentUser = $request->getUser();

$dashboardViews = Repo::submission()->getDashboardViews($context, $currentUser);

return $response->withJson($dashboardViews->map(fn(DashboardView $view) => $view->getCount()), 200);
}

/**
Expand Down Expand Up @@ -361,4 +441,10 @@ protected function getSubmissionCollector(array $queryParams): Collector

return $collector;
}

protected function canAccessAllSubmissions(): bool
{
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
return !empty(array_intersect([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $userRoles));
}
}
206 changes: 168 additions & 38 deletions classes/submission/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ abstract class Collector implements CollectorInterface
/** @var array|int */
public $assignedTo = null;
public array|int|null $isReviewedBy = null;

public ?array $reviewersNumber = null;
public ?bool $awaitingReviews = null;
public ?bool $reviewsSubmitted = null;
public ?bool $revisionsRequested = null;

public function __construct(DAO $dao)
{
Expand Down Expand Up @@ -203,12 +205,31 @@ public function filterByDaysInactive(?int $daysInactive): AppCollector
* Review assignment is considered active after the request is sent by the reviewer
* and it isn't cancelled, declined or overdue
*/
public function filterByReviewersActive(?int $reviewersNumber)
public function filterByReviewersActive(?array $reviewersNumber): AppCollector
{
$this->reviewersNumber = $reviewersNumber;
return $this;
}

/**
* Limit results by submission in the review stage having completed review assignments
*/
public function filterByReviewsSubmitted(?bool $hasSubmittedReviews): AppCollector
{
$this->reviewsSubmitted = $hasSubmittedReviews;
return $this;
}

/**
* Limit results by submissions in the review stage with review requests which hasn't been yet considered and pending review assignments
*/
public function filterByAwaitingReviews(?bool $hasAwaitingReviews): AppCollector
{
$this->awaitingReviews = $hasAwaitingReviews;
return $this;
}


/**
* Limit results to submissions assigned to these users
*
Expand Down Expand Up @@ -574,42 +595,7 @@ public function getQueryBuilder(): Builder
->whereIn('pc.category_id', $this->categoryIds);
}

// Filter by is reviewed by
if ($this->isReviewedBy !== null) {
// TODO consider review round and other criteria; refactor query builder to use ->when
$q->whereIn('s.stage_id', [WORKFLOW_STAGE_ID_EXTERNAL_REVIEW, WORKFLOW_STAGE_ID_INTERNAL_REVIEW])
->when(
$this->isReviewedBy === self::UNASSIGNED,
// Submission considered not under the review when ...
fn (Builder $q) => $q
// review assignments don't exist for the submission or exist but are declined or cancelled
->whereNotIn('s.submission_id', fn (Builder $q) => $q
->select('ra.submission_id')
->from('review_assignments AS ra')
->where('declined', 0)
->where('canceled', 0)
->distinct()),
fn (Builder $q) => $q
->whereIn('s.submission_id', fn (Builder $q) => $q
->select('ra.submission_id')
->from('review_assignments AS ra')
->whereIn('reviewer_id', (array) $this->isReviewedBy)
->where('declined', 0)
->where('canceled', 0)
)
);
}

$q->when($this->reviewersNumber !== null, fn (Builder $q) => $q
->whereIn('s.submission_id', fn(Builder $q) => $q
->select('ra.submission_id', DB::raw('count(*) as number'))
->groupBy('ra.submission_id')
->from('review_assignments AS ra')
->where('declined', 0)
->where('cancel', 0)
->havingRaw('number IN ?', $this->reviewersNumber)
)
);
$q = $this->buildReviewStageQueries($q);

// By any child pub object's DOI status
// Filter by any child pub object's DOI status
Expand All @@ -634,4 +620,148 @@ public function getQueryBuilder(): Builder

return $q;
}

/**
* Builds a single query to retrieve submissions count for all dashboard views
* @param Collection [
* Dashboard view unique ID => Submission Collector with filters applied
* ]
*/
public static function getViewsCountBuilder(Collection $keyCollectorPair): Builder
{
$q = DB::query();
$keyCollectorPair->each(function(AppCollector $collector, string $key) use ($q) {
// Get query builder from a collector instance, override a select statement to retrieve submissions count instead of submissions data
$subQuery = $collector->getQueryBuilder()->select([])->selectRaw('COUNT(s.submission_id)');
$q->selectSub($subQuery, $key);
});
return $q;
}

/**
* Build queries to retrieve review stage related
*/
protected function buildReviewStageQueries(Builder $q): Builder
{
$reviewFilters = collect([$this->isReviewedBy, $this->reviewersNumber, $this->awaitingReviews, $this->reviewsSubmitted, $this->revisionsRequested])->filter();
if ($reviewFilters->isEmpty()) {
return $q;
}

$reviewStageFilters = array_intersect($this->getReviewStages(), $this->stageIds ?? []);
$stagesToFilter = array_diff($this->getReviewStages(), $reviewStageFilters);
if (!empty($stagesToFilter)) {
$q->whereIn('s.stage_id', $stagesToFilter);
}

// Aggregate current review round number, don't include review assignments in non-relevant rounds
$currentReviewRound = DB::table('review_rounds', 'rr')
->select('rr.submission_id')
->selectRaw('MAX(rr.round) as current_round')
->groupBy('rr.submission_id');

$q->when($this->isReviewedBy !== null, fn (Builder $q) =>
$q->whereIn('s.submission_id', fn (Builder $q) => $q
->select('ra.submission_id')
->from('review_assignments AS ra')
->joinSub($currentReviewRound, 'agrr', fn(JoinClause $join) =>
$join->on('ra.submission_id', '=', 'agrr.submission_id')
)
->whereIn('reviewer_id', (array) $this->isReviewedBy)
->where('declined', 0)
->where('cancelled', 0)
)
);

$q->when($this->reviewersNumber !== null, function (Builder $q) use ($currentReviewRound) {
$reviewersNumber = $this->reviewersNumber;
$includeUnassigned = false;
if (in_array(0, $reviewersNumber)) {
$reviewersNumber = array_diff($reviewersNumber, [0]);
$includeUnassigned = true;
}

$q
->when($includeUnassigned, fn (Builder $q) => $q
->whereNotIn('s.submission_id', fn (Builder $q) => $q
->select('ra.submission_id')
->from('review_assignments AS ra')
->joinSub($currentReviewRound, 'agrr', fn(JoinClause $join) =>
$join->on('ra.submission_id', '=', 'agrr.submission_id')
)
->where('ra.declined', 0)
->where('ra.cancelled', 0)
->whereRaw('ra.round = agrr.current_round')
->distinct()
)
)
->when(!empty($reviewersNumber), function (Builder $q) use ($reviewersNumber, $currentReviewRound) {
$placeholders = array_fill(0, count($reviewersNumber), '?');

// Aggregate review assignments count per submission
$assignmentsPerSubmission = DB::table('review_assignments', 'ra')
->select('ra.submission_id')
->selectRaw('COUNT(ra.submission_id) as number')
->where('ra.declined', 0)
->where('ra.cancelled', 0)
->groupBy('ra.submission_id')
// Can't replace a single placeholder with array bindings, issue looks similar to laravel/framework#39554
->havingRaw('number IN (' . implode(',', $placeholders) . ')', $reviewersNumber);
$q->whereIn('s.submission_id', fn(Builder $q) => $q
// review assignments exist, counting the number of active assignments
->select('agra.submission_id')
->fromSub($assignmentsPerSubmission, 'agra')
->joinSub($currentReviewRound, 'agrr', fn(JoinClause $join) =>
$join->on('agra.submission_id', '=', 'agrr.submission_id')
)
);
});
});

$q->when($this->awaitingReviews !== null, fn(Builder $q) => $q
->whereIn('s.submission_id', fn(Builder $q) => $q
->select('ra.submission_id')
->from('review_assignments AS ra')
->joinSub($currentReviewRound, 'agrr', fn(JoinClause $join) =>
$join->on('ra.submission_id', '=', 'agrr.submission_id')
)
->whereNull('ra.date_completed')
->where('ra.cancelled', 0)
->where('ra.declined', 0)
->whereRaw('ra.round = agrr.current_round')
)
);

$q->when($this->reviewsSubmitted !== null, fn(Builder $q) => $q
->whereIn('s.submission_id', fn(Builder $q) => $q
->select('agrr.submission_id')
->from('review_assignments AS ra')
->joinSub($currentReviewRound, 'agrr', fn(JoinClause $join) =>
$join->on('ra.submission_id', '=', 'agrr.submission_id')
)
->whereNotNull('ra.date_completed')
->whereRaw('ra.round = agrr.current_round')
// TODO include only those, which weren't confirmed by the editor?
)
);

$q->when($this->revisionsRequested !== null, fn(Builder $q) => $q
->whereIn('s.submission_id', fn(Builder $q) => $q
->select('rr.submission_id')
->from('review_rounds AS rr')
->joinSub($currentReviewRound, 'agrr', fn(JoinClause $join) =>
$join->on('rr.submission_id', '=', 'agrr.submission_id')
)
->whereRaw('rr.round = agrr.curent_round')
->where('rr.status', ReviewRound::REVIEW_ROUND_STATUS_REVISIONS_REQUESTED)
)
);

return $q;
}

protected function getReviewStages(): array
{
return [WORKFLOW_STAGE_ID_EXTERNAL_REVIEW];
}
}
Loading

0 comments on commit 12c9dfb

Please sign in to comment.