Skip to content

pkp/pkp-lib#1660 Customizable Reviewer Recommendations #4505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/v1/contexts/index.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/**
* @defgroup api_v1_contexts Context API requests
*/
Expand Down
15 changes: 15 additions & 0 deletions api/v1/reviewers/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/**
* @file api/v1/reviewers/index.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @brief Handle API requests context's reviewers recommendations
*/

return new \PKP\handler\APIHandler(
new \APP\API\v1\reviewers\recommendations\ReviewerRecommendationController()
);
219 changes: 219 additions & 0 deletions api/v1/reviewers/recommendations/ReviewerRecommendationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

/**
* @file api/v1/reviewers/recommendations/ReviewerRecommendationController.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewerRecommendationController
*
* @brief API controller class to handle actions on reviewer recommendations
*
*/

namespace APP\API\v1\reviewers\recommendations;

use APP\API\v1\reviewers\recommendations\formRequests\AddReviewerRecommendation;
use APP\API\v1\reviewers\recommendations\formRequests\EditReviewerRecommendation;
use APP\API\v1\reviewers\recommendations\formRequests\UpdateStatusReviewerRecommendation;
use APP\API\v1\reviewers\recommendations\resources\ReviewerRecommendationResource;
use APP\core\Application;
use APP\security\authorization\RecommendationAccessPolicy;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\reviewer\recommendation\ReviewerRecommendation;

class ReviewerRecommendationController extends PKPBaseController
{
/**
* @copydoc \PKP\core\PKPBaseController::getHandlerPath()
*/
public function getHandlerPath(): string
{
return 'reviewers/recommendations';
}

/**
* @copydoc \PKP\core\PKPBaseController::getRouteGroupMiddleware()
*/
public function getRouteGroupMiddleware(): array
{
return [
'has.user',
'has.context',
self::roleAuthorizer([
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
]),
];
}

/**
* @copydoc \PKP\core\PKPBaseController::authorize()
*/
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
{
$illuminateRequest = $args[0]; /** @var \Illuminate\Http\Request $illuminateRequest */
$actionName = static::getRouteActionName($illuminateRequest);

$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));

if (in_array($actionName, ['get', 'edit', 'updateStatus', 'delete'])) {
$this->addPolicy(
new RecommendationAccessPolicy(
$request,
static::getRequestedRoute($illuminateRequest)->parameter('reviewerRecommendationId')
)
);
}

return parent::authorize($request, $args, $roleAssignments);
}

/**
* @copydoc \PKP\core\PKPBaseController::getGroupRoutes()
*/
public function getGroupRoutes(): void
{
Route::get('{reviewerRecommendationId}', $this->get(...))
->name('reviewer.recommendations.get')
->whereNumber(['reviewerRecommendationId']);

Route::get('', $this->getMany(...))
->name('reviewer.recommendations.getMany');

Route::post('', $this->add(...))
->name('reviewer.recommendations.add');

Route::put('{reviewerRecommendationId}', $this->edit(...))
->name('reviewer.recommendations.edit')
->whereNumber(['reviewerRecommendationId']);

Route::put('{reviewerRecommendationId}/status', $this->updateStatus(...))
->name('reviewer.recommendations.edit.status')
->whereNumber(['reviewerRecommendationId']);

Route::delete('{reviewerRecommendationId}', $this->delete(...))
->name('reviewer.recommendations.delete')
->whereNumber(['reviewerRecommendationId']);
}

/**
* Get specific recommendation response
*/
public function get(Request $illuminateRequest): JsonResponse
{
$recommendation = ReviewerRecommendation::find($illuminateRequest->route('reviewerRecommendationId'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should account for a recommendation not being found, returning a 404 if none is found.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RIght , this should get included in in_array($actionName, ['get', 'edit', 'updateStatus', 'delete']). Added it .


return response()->json(
(new ReviewerRecommendationResource($recommendation))->toArray($illuminateRequest),
Response::HTTP_OK
);
}

/**
* Get all recommendations response
*/
public function getMany(Request $illuminateRequest): JsonResponse
{
$recommendations = ReviewerRecommendation::query()
->withContextId(Application::get()->getRequest()->getContext()->getId())
->get();

return response()->json([
'items' => ReviewerRecommendationResource::collection($recommendations),
'itemMax' => $recommendations->count(),
], Response::HTTP_OK);
}

/**
* Add new recommendation
*/
public function add(AddReviewerRecommendation $illuminateRequest): JsonResponse
{
$validateds = $illuminateRequest->validated();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in variable name. Also, if the data is not validated, do we manually need to return a different response here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in variable name

Is this for $validateds ? this is intentionally done as plural from of validated so identify as all validated data .

if the data is not validated, do we manually need to return a different response here?

As we are using FromRequest as AddReviewerRecommendation, so any failed validation will be handled there and return back from there without reaching to controller code . Here we will get validated data after successful validation, so no need to return different response as will never reach here without completing the validation successfully .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I thought that was probably the case but I didn't know if how we were using it would handle all that automatically.


$recommendation = ReviewerRecommendation::create($validateds);

return response()->json(
(new ReviewerRecommendationResource($recommendation->refresh()))
->toArray($illuminateRequest),
Response::HTTP_OK
);
}

/**
* Update existing recommendation
*/
public function edit(EditReviewerRecommendation $illuminateRequest): JsonResponse
{
$validated = $illuminateRequest->validated();

$recommendation = ReviewerRecommendation::find($illuminateRequest->route('reviewerRecommendationId'));

if (!$recommendation->removable) {
return response()->json([
'error' => __('api.406.notAcceptable'),
], Response::HTTP_NOT_ACCEPTABLE);
}

if (!$recommendation->update($validated)) {
return response()->json([
'error' => __('api.409.resourceActionConflict'),
], Response::HTTP_CONFLICT);
}

return response()->json(
(new ReviewerRecommendationResource($recommendation->refresh()))
->toArray($illuminateRequest),
Response::HTTP_OK
);
}

/**
* Update the status of existing recommendation
*/
public function updateStatus(UpdateStatusReviewerRecommendation $illuminateRequest): JsonResponse
{
$validated = $illuminateRequest->validated();

$recommendation = ReviewerRecommendation::find($illuminateRequest->route('reviewerRecommendationId'));

$recommendation->update($validated);

return response()->json(
(new ReviewerRecommendationResource($recommendation->refresh()))
->toArray($illuminateRequest),
Response::HTTP_OK
);
}

/**
* Delete existing recommendation
*/
public function delete(Request $illuminateRequest): JsonResponse
{
$recommendation = ReviewerRecommendation::find($illuminateRequest->route('reviewerRecommendationId'));

if (!$recommendation->removable) {
return response()->json([
'error' => __('api.406.notAcceptable'),
], Response::HTTP_NOT_ACCEPTABLE);
}

$recommendation->delete();

return response()->json([], Response::HTTP_OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/**
* @file api/v1/reviewers/recommendations/formRequests/AddReviewerRecommendation.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AddReviewerRecommendation
*
* @brief Form request class to validation storing of resource
*
*/

namespace APP\API\v1\reviewers\recommendations\formRequests;

use APP\core\Application;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use PKP\submission\reviewer\recommendation\ReviewerRecommendation;
use PKP\validation\traits\HasMultilingualRule;

class AddReviewerRecommendation extends FormRequest
{
use HasMultilingualRule;

/**
* @copydoc \PKP\validation\traits\HasMultilingualRule::multilingualInputs()
*/
public function multilingualInputs(): array
{
return (new ReviewerRecommendation())->getMultilingualProps();
}

/**
* @copydoc \PKP\validation\traits\HasMultilingualRule::primaryLocale()
*/
public function primaryLocale(): ?string
{
return Application::get()->getRequest()->getContext()->getPrimaryLocale();
}

/**
* @copydoc \PKP\validation\traits\HasMultilingualRule::allowedLocales()
*/
public function allowedLocales(): array
{
return Application::get()->getRequest()->getContext()->getSupportedFormLocales();
}

/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$contextDao = Application::getContextDAO();

return [
'contextId' => [
'required',
'integer',
Rule::exists($contextDao->tableName, $contextDao->primaryKeyColumn),
],
'title' => [
'required',
],
'status' => [
'required',
'boolean'
],
];
}

/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'contextId' => Application::get()->getRequest()->getContext()->getId(),
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* @file api/v1/reviewers/recommendations/formRequests/EditReviewerRecommendation.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class EditReviewerRecommendation
*
* @brief Form request class to validation updating of resource
*
*/

namespace APP\API\v1\reviewers\recommendations\formRequests;

class EditReviewerRecommendation extends AddReviewerRecommendation
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => [
'required',
],
'status' => [
'required',
'boolean'
],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/**
* @file api/v1/reviewers/recommendations/formRequests/UpdateStatusReviewerRecommendation.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class UpdateStatusReviewerRecommendation
*
* @brief Form request class to validation updating of resource status
*
*/

namespace APP\API\v1\reviewers\recommendations\formRequests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateStatusReviewerRecommendation extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'status' => [
'required',
'boolean'
],
];
}
}
Loading
Loading