diff --git a/api/v1/reviewerSuggestions/formRequests/AddReviewerSuggestion.php b/api/v1/reviewerSuggestions/formRequests/AddReviewerSuggestion.php
deleted file mode 100644
index f319734225d..00000000000
--- a/api/v1/reviewerSuggestions/formRequests/AddReviewerSuggestion.php
+++ /dev/null
@@ -1,65 +0,0 @@
- [
- 'required',
- 'string',
- 'max:255',
- ],
- 'givenName' => [
- 'required',
- 'string',
- 'max:255',
- ],
- 'email' => [
- 'required',
- 'email',
- ],
- ];
- }
-
- /**
- * Get the error messages for the defined validation rules.
- *
- * @return array
- */
- public function messages(): array
- {
- return [
- 'familyName.required' => 'family name is required',
- ];
- }
-
- /**
- * Get custom attributes for validator errors.
- *
- * @return array
- */
- public function attributes(): array
- {
- return [
- 'familyName' => __('user.familyName'),
- 'givenName' => __('user.givenName'),
- 'email' => __('user.email'),
- ];
- }
-}
diff --git a/api/v1/reviewerSuggestions/ReviewerSuggestionController.php b/api/v1/reviewers/suggestions/ReviewerSuggestionController.php
similarity index 75%
rename from api/v1/reviewerSuggestions/ReviewerSuggestionController.php
rename to api/v1/reviewers/suggestions/ReviewerSuggestionController.php
index d9f6573d23d..63b9b94f45b 100644
--- a/api/v1/reviewerSuggestions/ReviewerSuggestionController.php
+++ b/api/v1/reviewers/suggestions/ReviewerSuggestionController.php
@@ -13,28 +13,25 @@
*
*/
-namespace PKP\API\v1\reviewerSuggestions;
+namespace PKP\API\v1\reviewers\suggestions;
use APP\core\Application;
-use PKP\API\v1\reviewerSuggestions\formRequests\AddReviewerSuggestion;
+use PKP\API\v1\reviewers\suggestions\resources\ReviewerSuggestionResource;
-use PKP\security\authorization\internal\SubmissionIncompletePolicy;
use APP\facades\Repo;
+use PKP\API\v1\reviewers\suggestions\formRequests\AddReviewerSuggestion;
+use PKP\security\authorization\internal\SubmissionIncompletePolicy;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
-use PKP\core\PKPApplication;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
-use PKP\db\DAORegistry;
-use PKP\log\EmailLogEntry;
-use PKP\log\SubmissionEmailLogEventType;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
-use PKP\submissionFile\SubmissionFile;
+use PKP\submission\reviewer\suggestion\ReviewerSuggestion;
class ReviewerSuggestionController extends PKPBaseController
{
@@ -43,7 +40,7 @@ class ReviewerSuggestionController extends PKPBaseController
*/
public function getHandlerPath(): string
{
- return 'reviewerSuggestions';
+ return 'submissions/{submissionId}/reviewers/suggestions';
}
/**
@@ -74,11 +71,11 @@ public function getGroupRoutes(): void
->name('reviewer.suggestions.get')
->whereNumber('suggestionId');
- Route::get('submission/{submissionId}', $this->getMany(...))
+ Route::get('', $this->getMany(...))
->name('reviewer.suggestions.getMany')
->whereNumber('submissionId');
- Route::post('submission/{submissionId}', $this->add(...))
+ Route::post('', $this->add(...))
->name('reviewer.suggestions.add');
Route::put('{suggestionId}', $this->edit(...))
@@ -114,30 +111,40 @@ public function authorize(PKPRequest $request, array &$args, array $roleAssignme
public function get(Request $illuminateRequest): JsonResponse
{
- $request = $this->getRequest();
- $context = $request->getContext();
- $contextId = $context->getId();
+ $suggestion = ReviewerSuggestion::find($illuminateRequest->route('suggestionId'));
+
+ if (!$suggestion) {
+ return response()->json([
+ 'error' => __('api.404.resourceNotFound'),
+ ], Response::HTTP_NOT_FOUND);
+ }
- return response()->json([], Response::HTTP_OK);
+ return response()->json(
+ (new ReviewerSuggestionResource($suggestion))->toArray($illuminateRequest),
+ Response::HTTP_OK
+ );
}
public function getMany(Request $illuminateRequest): JsonResponse
{
- $request = $this->getRequest();
- $context = $request->getContext();
- $contextId = $context->getId();
+ $suggestions = ReviewerSuggestion::withSubmissionId($illuminateRequest->route('sibmissionId'))->get();
- return response()->json([], Response::HTTP_OK);
+ return response()->json([
+ 'items' => ReviewerSuggestionResource::collection($suggestions),
+ 'itemMax' => $suggestions->count(),
+ ], Response::HTTP_OK);
}
public function add(AddReviewerSuggestion $illuminateRequest): JsonResponse
{
$validateds = $illuminateRequest->validated();
- $request = $this->getRequest();
- $context = $request->getContext();
- $contextId = $context->getId();
+
+ $suggestion = ReviewerSuggestion::create($validateds);
- return response()->json([], Response::HTTP_OK);
+ return response()->json(
+ (new ReviewerSuggestionResource($suggestion))->toArray($illuminateRequest),
+ Response::HTTP_OK
+ );
}
public function edit(Request $illuminateRequest): JsonResponse
diff --git a/api/v1/reviewers/suggestions/formRequests/AddReviewerSuggestion.php b/api/v1/reviewers/suggestions/formRequests/AddReviewerSuggestion.php
new file mode 100644
index 00000000000..c0a9424205d
--- /dev/null
+++ b/api/v1/reviewers/suggestions/formRequests/AddReviewerSuggestion.php
@@ -0,0 +1,107 @@
+map(
+ fn($value) => is_array($value) ? array_filter($value) : $value
+ )->toArray();
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ */
+ public function rules(): array
+ {
+ return [
+ 'submissionId' => [
+ 'required',
+ 'integer',
+ Rule::exists('submissions', 'submission_id'),
+ ],
+ 'suggestingUserId' => [
+ 'sometimes',
+ 'nullable',
+ 'integer',
+ Rule::exists('users', 'user_id'),
+ ],
+ 'familyName' => [
+ 'required',
+ 'array',
+ ],
+ 'givenName' => [
+ 'required',
+ 'array',
+ ],
+ 'email' => [
+ 'required',
+ 'email',
+ ],
+ 'affiliation' => [
+ 'required',
+ 'array',
+ ],
+ 'suggestionReason' => [
+ 'required',
+ 'array',
+ ],
+ ];
+ }
+
+ /**
+ * Get the error messages for the defined validation rules.
+ *
+ * @return array
+ */
+ public function messages(): array
+ {
+ return [
+ 'familyName.required' => 'family name is required',
+ ];
+ }
+
+ /**
+ * Get custom attributes for validator errors.
+ *
+ * @return array
+ */
+ public function attributes(): array
+ {
+ return [
+ 'familyName' => __('user.familyName'),
+ 'givenName' => __('user.givenName'),
+ 'email' => __('user.email'),
+ ];
+ }
+
+ /**
+ * Prepare the data for validation.
+ */
+ protected function prepareForValidation(): void
+ {
+ $this->merge([
+ 'suggestingUserId' => Application::get()->getRequest()?->getUser()?->getId(),
+ 'submissionId' => $this->route('submissionId'),
+ ]);
+ }
+}
diff --git a/api/v1/reviewers/suggestions/resources/ReviewerSuggestionResource.php b/api/v1/reviewers/suggestions/resources/ReviewerSuggestionResource.php
new file mode 100644
index 00000000000..f777734bc08
--- /dev/null
+++ b/api/v1/reviewers/suggestions/resources/ReviewerSuggestionResource.php
@@ -0,0 +1,28 @@
+ $this->id,
+ 'submissionId' => $this->submissionId,
+ 'suggestingUserId' => $this->suggestingUserId,
+ 'familyName' => $this->familyName,
+ 'givenName' => $this->givenName,
+ 'fullName' => $this->fullname,
+ 'email' => $this->email,
+ 'orcidId' => $this->orcidId,
+ 'affiliation' => $this->affiliation,
+ 'suggestionReason' => $this->suggestionReason,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/classes/components/forms/publication/ReviewerSuggestionsForm.php b/classes/components/forms/publication/ReviewerSuggestionsForm.php
new file mode 100644
index 00000000000..9541cf409b6
--- /dev/null
+++ b/classes/components/forms/publication/ReviewerSuggestionsForm.php
@@ -0,0 +1,82 @@
+action = $action;
+ $this->locales = $locales;
+ $this->submission = $submission;
+ $this->context = $context;
+
+ $this
+ ->addField(new FieldText('givenName', [
+ 'label' => __('user.givenName'),
+ 'isMultilingual' => true,
+ 'isRequired' => true,
+ ]))
+ ->addField(new FieldText('familyName', [
+ 'label' => __('user.familyName'),
+ 'isMultilingual' => true,
+ 'isRequired' => true,
+ ]))
+ ->addField(new FieldText('email', [
+ 'label' => __('user.email'),
+ 'isRequired' => true,
+ ]))
+ ->addField(new FieldText('affiliation', [
+ 'label' => __('user.affiliation'),
+ 'isRequired' => true,
+ 'isMultilingual' => true,
+ ]))
+ ->addField(new FieldRichTextarea('suggestionReason', [
+ 'label' => __('reviewerSuggestion.suggestionReason'),
+ 'description' => __('reviewerSuggestion.suggestionReason.description'),
+ 'isRequired' => true,
+ 'isMultilingual' => true,
+ ]));
+
+ if (OrcidManager::isEnabled()) {
+ $this->addField(new FieldOrcid('orcid', [
+ 'label' => __('user.orcid'),
+ 'tooltip' => __('orcid.about.orcidExplanation'),
+ ]), [FIELD_POSITION_AFTER, 'email']);
+ }
+ }
+}
diff --git a/classes/components/forms/submission/SubmissionGuidanceSettings.php b/classes/components/forms/submission/SubmissionGuidanceSettings.php
index 471357060c7..ae0ca835710 100644
--- a/classes/components/forms/submission/SubmissionGuidanceSettings.php
+++ b/classes/components/forms/submission/SubmissionGuidanceSettings.php
@@ -41,14 +41,15 @@ public function __construct(string $action, array $locales, Context $context)
'submissions'
);
- $this->addField(new FieldRichTextarea('authorGuidelines', [
- 'label' => __('manager.setup.authorGuidelines'),
- 'description' => __('manager.setup.authorGuidelines.description', ['url' => $submissionUrl]),
- 'isMultilingual' => true,
- 'value' => $context->getData('authorGuidelines'),
- 'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist',
- 'plugins' => 'paste,link,lists',
- ]))
+ $this
+ ->addField(new FieldRichTextarea('authorGuidelines', [
+ 'label' => __('manager.setup.authorGuidelines'),
+ 'description' => __('manager.setup.authorGuidelines.description', ['url' => $submissionUrl]),
+ 'isMultilingual' => true,
+ 'value' => $context->getData('authorGuidelines'),
+ 'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist',
+ 'plugins' => 'paste,link,lists',
+ ]))
->addField(new FieldRichTextarea('beginSubmissionHelp', [
'label' => __('submission.wizard.beforeStart'),
'description' => __('manager.setup.workflow.beginSubmissionHelp.description'),
@@ -113,5 +114,28 @@ public function __construct(string $action, array $locales, Context $context)
'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist',
'plugins' => 'paste,link,lists',
]));
+
+ $this->addReviewSuggestionGuidanceDetail($this->context);
+ }
+
+ /**
+ * Add review suggestion guidance text
+ */
+ protected function addReviewSuggestionGuidanceDetail(Context $context): void
+ {
+ $schema = app()->get('schema'); /** @var \PKP\services\PKPSchemaService $schema */
+
+ if (!collect($schema->get("context")->properties)->has("reviewerSuggestionEnabled")) {
+ return;
+ }
+
+ $this->addField(new FieldRichTextarea('reviewerSuggestionsHelp', [
+ 'label' => __('submission.forReviewerSuggestion'),
+ 'description' => __('manager.setup.workflow.reviewerSuggestionsHelp.description'),
+ 'isMultilingual' => true,
+ 'value' => $context->getData('reviewerSuggestionsHelp'),
+ 'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist',
+ 'plugins' => 'paste,link,lists',
+ ]));
}
}
diff --git a/classes/components/listPanels/ReviewerSuggestionsListPanel.php b/classes/components/listPanels/ReviewerSuggestionsListPanel.php
new file mode 100644
index 00000000000..f1f9270b603
--- /dev/null
+++ b/classes/components/listPanels/ReviewerSuggestionsListPanel.php
@@ -0,0 +1,131 @@
+submission = $submission;
+ $this->context = $context;
+ $this->locales = $locales;
+ $this->items = $items;
+ $this->canEditPublication = $canEditPublication;
+ }
+
+ /**
+ * @copydoc ListPanel::getConfig()
+ */
+ public function getConfig()
+ {
+ $config = parent::getConfig();
+
+ // Remove some props not used in this list panel
+ unset($config['description']);
+ unset($config['expanded']);
+ unset($config['headingLevel']);
+
+ $config = array_merge(
+ $config,
+ [
+ 'canEditPublication' => $this->canEditPublication,
+ 'publicationApiUrlFormat' => $this->getPublicationUrlFormat(),
+ 'reviewerSuggestionsApiUrl' => $this->getReviewerSuggestionsApiUrl(),
+ 'form' => $this->getLocalizedForm(),
+ 'items' => $this->items,
+ ]
+ );
+
+ return $config;
+ }
+
+ protected function getReviewerSuggestionsApiUrl(): string
+ {
+ return Application::get()->getRequest()->getDispatcher()->url(
+ Application::get()->getRequest(),
+ Application::ROUTE_API,
+ $this->context->getPath(),
+ "submissions/{$this->submission->getId()}/reviewers/suggestions"
+ );
+ }
+
+ /**
+ * Get an example of the url to a publication's API endpoint,
+ * with a placeholder instead of the publication id, eg:
+ *
+ * http://example.org/api/v1/submissions/1/publications/__publicationId__
+ */
+ protected function getPublicationUrlFormat(): string
+ {
+ return Application::get()->getRequest()->getDispatcher()->url(
+ Application::get()->getRequest(),
+ Application::ROUTE_API,
+ $this->context->getPath(),
+ 'submissions/' . $this->submission->getId() . '/publications/__publicationId__'
+ );
+ }
+
+ /**
+ * Get the form data localized to the submission's locale
+ */
+ protected function getLocalizedForm(): array
+ {
+ $apiUrl = $this->getReviewerSuggestionsApiUrl();
+
+ $submissionLocale = $this->submission->getData('locale');
+ $data = $this->getForm($apiUrl)->getConfig();
+
+ $data['primaryLocale'] = $submissionLocale;
+ $data['visibleLocales'] = [$submissionLocale];
+ $data['supportedFormLocales'] = collect($this->locales)
+ ->sortBy([fn (array $a, array $b) => $b['key'] === $submissionLocale ? 1 : -1])
+ ->values()
+ ->toArray();
+
+ return $data;
+ }
+
+ /**
+ * Get the reviewer suggestions form
+ */
+ protected function getForm(string $url): ReviewerSuggestionsForm
+ {
+ return new ReviewerSuggestionsForm(
+ $url,
+ $this->locales,
+ $this->submission,
+ $this->context
+ );
+ }
+}
diff --git a/classes/core/PKPBaseController.php b/classes/core/PKPBaseController.php
index d1a2c64c9d9..4b0b35d431a 100644
--- a/classes/core/PKPBaseController.php
+++ b/classes/core/PKPBaseController.php
@@ -388,6 +388,11 @@ public function getParameter(string $parameterName, mixed $default = null): mixe
if (isset($queryParams[$parameterName])) {
return $queryParams[$parameterName];
}
+
+ $inputs = $illuminateRequest->input();
+ if (isset($inputs[$parameterName])) {
+ return $inputs[$parameterName];
+ }
}
return $default;
diff --git a/classes/core/traits/ModelWithSettings.php b/classes/core/traits/ModelWithSettings.php
index d077cf73bb3..3d0a0d2dbf5 100644
--- a/classes/core/traits/ModelWithSettings.php
+++ b/classes/core/traits/ModelWithSettings.php
@@ -46,7 +46,7 @@ abstract public function getTable();
/**
* Get settings table name
*/
- abstract public function getSettingsTable();
+ abstract public function getSettingsTable(): string;
/**
* The name of the schema for the Model if exists, null otherwise
diff --git a/classes/migration/install/ReviewerSuggestionsMigration.php b/classes/migration/install/ReviewerSuggestionsMigration.php
index d22c60d6d69..02785cc160a 100644
--- a/classes/migration/install/ReviewerSuggestionsMigration.php
+++ b/classes/migration/install/ReviewerSuggestionsMigration.php
@@ -29,15 +29,15 @@ public function up(): void
$table->bigInteger('reviewer_suggestion_id')->autoIncrement();
$table
- ->bigInteger('user_id')
+ ->bigInteger('suggesting_user_id')
->nullable()
->comment('The user/author who has made the suggestion');
$table
- ->foreign('user_id')
+ ->foreign('suggesting_user_id')
->references('user_id')
->on('users')
->onDelete('set null');
- $table->index(['user_id'], 'reviewer_suggestions_user_id');
+ $table->index(['suggesting_user_id'], 'reviewer_suggestions_suggesting_user_id');
$table->bigInteger('submission_id')->comment('Submission at which the suggestion was made');
$table->foreign('submission_id')->references('submission_id')->on('submissions')->onDelete('cascade');
@@ -54,7 +54,7 @@ public function up(): void
$table
->bigInteger('stage_id')
->nullable()
- ->comment('The stage at whihc suggestion approved');
+ ->comment('The stage at which suggestion approved');
$table
->bigInteger('approver_id')
diff --git a/classes/migration/upgrade/v3_5_0/I4787_ReviewSuggestions.php b/classes/migration/upgrade/v3_5_0/I4787_ReviewSuggestions.php
new file mode 100644
index 00000000000..c47e1664334
--- /dev/null
+++ b/classes/migration/upgrade/v3_5_0/I4787_ReviewSuggestions.php
@@ -0,0 +1,50 @@
+_installer, $this->_attributes))->up();
+ parent::up();
+ }
+
+ /**
+ * Reverse the migration
+ */
+ public function down(): void
+ {
+ (new ReviewerSuggestionsMigration($this->_installer, $this->_attributes))->down();
+ parent::down();
+ }
+
+ /**
+ * @return Collection [settingName => localeKey]
+ */
+ protected function getNewSettings(): Collection
+ {
+ return collect([
+ 'reviewerSuggestionsHelp' => 'default.submission.step.reviewerSuggestions',
+ ]);
+ }
+}
diff --git a/classes/publication/Repository.php b/classes/publication/Repository.php
index c829375ab29..ae1acbfa015 100644
--- a/classes/publication/Repository.php
+++ b/classes/publication/Repository.php
@@ -365,7 +365,7 @@ public function version(Publication $publication): int
$citationDao->importCitations($newPublication->getId(), $newPublication->getData('citationsRaw'));
}
- $genreDao = DAORegistry::getDAO('GenreDAO');
+ $genreDao = DAORegistry::getDAO('GenreDAO'); /** @var \PKP\submission\GenreDAO $genreDao */
$genres = $genreDao->getEnabledByContextId($context->getId());
$jatsFile = Repo::jats()
diff --git a/classes/submission/maps/Schema.php b/classes/submission/maps/Schema.php
index 0924f1915f1..a51ca00ce55 100644
--- a/classes/submission/maps/Schema.php
+++ b/classes/submission/maps/Schema.php
@@ -14,6 +14,8 @@
namespace PKP\submission\maps;
use APP\core\Application;
+use PKP\API\v1\reviewers\suggestions\resources\ReviewerSuggestionResource;
+
use APP\facades\Repo;
use APP\submission\Submission;
use Illuminate\Support\Collection;
@@ -28,6 +30,7 @@
use PKP\stageAssignment\StageAssignment;
use PKP\submission\Genre;
use PKP\submission\reviewAssignment\ReviewAssignment;
+use PKP\submission\reviewer\suggestion\ReviewerSuggestion;
use PKP\submission\reviewRound\ReviewRound;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
@@ -54,6 +57,9 @@ class Schema extends \PKP\core\maps\Schema
/** @var Enumerable Stage assignments associated with submissions. */
public Enumerable $stageAssignments;
+ /** @var Enumerable Reviewer Suggestions associated with submissions. */
+ public Enumerable $reviewerSuggestions;
+
/**
* Get extra property names used in the submissions list
*
@@ -115,12 +121,14 @@ public function map(
array $genres,
?Enumerable $reviewAssignments = null,
?Enumerable $stageAssignments = null,
- bool|Collection $anonymizeReviews = false
+ bool|Collection $anonymizeReviews = false,
+ ?Enumerable $reviewerSuggestions = null
): array {
$this->userGroups = $userGroups;
$this->genres = $genres;
$this->reviewAssignments = $reviewAssignments ?? Repo::reviewAssignment()->getCollector()->filterBySubmissionIds([$item->getId()])->getMany()->remember();
$this->stageAssignments = $stageAssignments ?? $this->getStageAssignmentsBySubmissions(collect([$item]), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]);
+ $this->reviewerSuggestions = $reviewerSuggestions ?? ReviewerSuggestion::withSubmissionIds($item->getId())->get();
return $this->mapByProperties($this->getProps(), $item, $anonymizeReviews);
}
@@ -143,11 +151,13 @@ public function summarize(
?Enumerable $reviewAssignments = null,
?Enumerable $stageAssignments = null,
bool|Collection $anonymizeReviews = false,
+ ?Enumerable $reviewerSuggestions = null
): array {
$this->userGroups = $userGroups;
$this->genres = $genres;
$this->reviewAssignments = $reviewAssignments ?? Repo::reviewAssignment()->getCollector()->filterBySubmissionIds([$item->getId()])->getMany()->remember();
$this->stageAssignments = $stageAssignments ?? $this->getStageAssignmentsBySubmissions(collect([$item]), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]);
+ $this->reviewerSuggestions = $reviewerSuggestions ?? ReviewerSuggestion::withSubmissionIds($item->getId())->get();
return $this->mapByProperties($this->getSummaryProps(), $item, $anonymizeReviews);
}
@@ -161,7 +171,12 @@ public function summarize(
* @param Genre[] $genres The file genres in this context
* @param bool|Collection $anonymizeReviews List of review assignment IDs to anonymize
*/
- public function mapMany(Enumerable $collection, LazyCollection $userGroups, array $genres, bool|Collection $anonymizeReviews = false): Enumerable
+ public function mapMany(
+ Enumerable $collection,
+ LazyCollection $userGroups,
+ array $genres,
+ bool|Collection $anonymizeReviews = false
+ ): Enumerable
{
$this->collection = $collection;
$this->userGroups = $userGroups;
@@ -173,6 +188,8 @@ public function mapMany(Enumerable $collection, LazyCollection $userGroups, arra
$associatedStageAssignments = $this->stageAssignments->groupBy(fn (StageAssignment $stageAssignment, int $key) =>
$stageAssignment->submissionId);
+ // TODO : Build up associated reviewer suggestions
+
return $collection->map(
fn ($item) =>
$this->map(
@@ -195,7 +212,12 @@ public function mapMany(Enumerable $collection, LazyCollection $userGroups, arra
* @param Genre[] $genres The file genres in this context
* @param bool|Collection $anonymizeReviews List of review assignment IDs to anonymize
*/
- public function summarizeMany(Enumerable $collection, LazyCollection $userGroups, array $genres, bool|Collection $anonymizeReviews = false): Enumerable
+ public function summarizeMany(
+ Enumerable $collection,
+ LazyCollection $userGroups,
+ array $genres,
+ bool|Collection $anonymizeReviews = false
+ ): Enumerable
{
$this->collection = $collection;
$this->userGroups = $userGroups;
@@ -212,6 +234,8 @@ public function summarizeMany(Enumerable $collection, LazyCollection $userGroups
$stageAssignment->submissionId
);
+ // TODO : Build up associated reviewer suggestions
+
return $collection->map(
fn ($item) =>
$this->summarize(
@@ -246,6 +270,8 @@ public function mapToSubmissionsList(
$this->genres = $genres;
$this->reviewAssignments = $reviewAssignments ?? Repo::reviewAssignment()->getCollector()->filterBySubmissionIds([$item->getId()])->getMany()->remember();
$this->stageAssignments = $stageAssignments ?? $this->getStageAssignmentsBySubmissions(collect([$item]), [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR]);
+ $this->reviewerSuggestions = $reviewerSuggestions ?? ReviewerSuggestion::withSubmissionIds($item->getId())->get();
+
return $this->mapByProperties($this->getSubmissionsListProps(), $item, $anonymizeReviews);
}
@@ -279,6 +305,8 @@ public function mapManyToSubmissionsList(
$stageAssignment->submissionId
);
+ // TODO : Build up associated reviewer suggestions
+
return $collection->map(
fn ($item) =>
$this->mapToSubmissionsList(
@@ -406,6 +434,9 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co
case 'urlWorkflow':
$output[$prop] = Repo::submission()->getWorkflowUrlByUserRoles($submission);
break;
+ case 'reviewerSuggestions':
+ $output[$prop] = $this->getPropertyReviewerSuggestions($this->reviewerSuggestions);
+ break;
default:
$output[$prop] = $submission->getData($prop);
break;
@@ -415,6 +446,19 @@ protected function mapByProperties(array $props, Submission $submission, bool|Co
return $output;
}
+ /**
+ * Get details about the reviewer suggestions for a submission
+ */
+ protected function getPropertyReviewerSuggestions(Enumerable $reviewerSuggestions): array
+ {
+ // TODO : why index start from 1 instead of 0
+ // this cause the transformation to Object instead of Array in JS side
+ return array_values(
+ ReviewerSuggestionResource::collection($reviewerSuggestions)
+ ->toArray(app()->get("request"))
+ );
+ }
+
/**
* Get details about the review assignments for a submission
*/
diff --git a/classes/submission/reviewer/suggestion/ReviewerSuggestion.php b/classes/submission/reviewer/suggestion/ReviewerSuggestion.php
index 090dbecd087..b265e8f5299 100644
--- a/classes/submission/reviewer/suggestion/ReviewerSuggestion.php
+++ b/classes/submission/reviewer/suggestion/ReviewerSuggestion.php
@@ -14,36 +14,67 @@
namespace PKP\submission\reviewer\suggestion;
-use Illuminate\Database\Eloquent\Model;
use APP\facades\Repo;
use Carbon\Carbon;
-use Eloquence\Behaviours\HasCamelCasing;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Arr;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
-
+use PKP\core\traits\ModelWithSettings;
class ReviewerSuggestion extends Model
{
- use HasCamelCasing;
+ use ModelWithSettings;
protected $table = 'reviewer_suggestions';
protected $primaryKey = 'reviewer_suggestion_id';
- protected $fillable = [
-
- ];
+ protected $guarded = [];
protected function casts(): array
{
return [
- 'userId' => 'int',
- 'submissionId' => 'int',
- 'email' => 'string',
- 'orcidId' => 'string',
- 'approvedAt' => 'datetime',
- 'stageId' => 'int',
- 'approverId' => 'int',
- 'reviewerId' => 'int',
+ 'suggestingUserId' => 'int',
+ 'submissionId' => 'int',
+ 'email' => 'string',
+ 'orcidId' => 'string',
+ 'approvedAt' => 'datetime',
+ 'stageId' => 'int',
+ 'approverId' => 'int',
+ 'reviewerId' => 'int',
+ ];
+ }
+
+ public function getSettingsTable(): string
+ {
+ return 'reviewer_suggestion_settings';
+ }
+
+ public static function getSchemaName(): ?string
+ {
+ return null;
+ }
+
+ // TODO Add instution details as setting
+ public function getSettings(): array
+ {
+ return [
+ 'familyName',
+ 'givenName',
+ 'affiliation',
+ 'suggestionReason',
+ ];
+ }
+
+ // TODO should the instution details be a multigingual prop
+ public function getMultilingualProps(): array
+ {
+ return [
+ 'fullName',
+ 'familyName',
+ 'givenName',
+ 'affiliation',
+ 'suggestionReason',
];
}
@@ -52,6 +83,21 @@ public function hasApproved(): ?Carbon
return $this->approvedAt;
}
+ /**
+ * Get the full name
+ */
+ protected function fullName(): Attribute
+ {
+ return Attribute::make(
+ get: function (mixed $value, array $attributes) {
+ $familyName = $this->familyName;
+ return collect($this->givenName)
+ ->map(fn ($givenName, $locale) => $givenName. ' ' . $familyName[$locale])
+ ->toArray();
+ }
+ );
+ }
+
/**
* Accessor and Mutator for primary key => id
*/
@@ -60,74 +106,61 @@ protected function id(): Attribute
return Attribute::make(
get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null,
set: fn($value) => [$this->primaryKey => $value],
- );
+ )->shouldCache();
}
- /**
- * Accessor for submission.
- * Should replace with relationship once Submission is converted to an Eloquent Model.
- */
protected function submission(): Attribute
{
return Attribute::make(
get: fn () => Repo::submission()->get($this->submissionId, true),
- );
+ )->shouldCache();
}
- /**
- * Accessor for user.
- * Should replace with relationship once User is converted to an Eloquent Model.
- */
- protected function user(): Attribute
+ protected function suggestingUser(): Attribute
{
return Attribute::make(
- get: fn () => Repo::user()->get($this->userId, true),
- );
+ get: fn () => $this->suggestingUserId
+ ? Repo::user()->get($this->suggestingUserId, true)
+ : null
+ )->shouldCache();
}
- /**
- * Accessor for user.
- * Should replace with relationship once User is converted to an Eloquent Model.
- */
- protected function approver(): ?Attribute
+ protected function approver(): Attribute
{
return Attribute::make(
get: fn () => $this->approverId ? Repo::user()->get($this->approverId, true) : null,
- );
+ )->shouldCache();
}
- /**
- * Accessor for user.
- * Should replace with relationship once User is converted to an Eloquent Model.
- */
- protected function reviewer(): ?Attribute
+ protected function reviewer(): Attribute
{
return Attribute::make(
get: fn () => $this->reviewerId ? Repo::user()->get($this->reviewerId, true) : null,
- );
+ )->shouldCache();
}
public function scopeWithContextId(Builder $query, int $contextId): Builder
{
- return $query->whereIn("submission_id", fn (Builder $query) => $query
- ->select("submission_id")
- ->from("submissions")
- ->where("context_id", $contextId)
- );
+ return $query
+ ->whereIn('submission_id', fn (Builder $query) => $query
+ ->select('submission_id')
+ ->from('submissions')
+ ->where('context_id', $contextId)
+ );
}
- public function scopeWithSubmissionId(Builder $query, int $submissionId): Builder
+ public function scopeWithSubmissionIds(Builder $query, int|array $submissionId): Builder
{
- return $query->where("submission_id", $submissionId);
+ return $query->whereIn('submission_id', Arr::wrap($submissionId));
}
- public function scopeWithUserId(Builder $query, int $userId): Builder
+ public function scopeWithSuggestingUserIds(Builder $query, int|array $userId): Builder
{
- return $query->where("user_id", $userId);
+ return $query->whereIn('suggesting_user_id', Arr::wrap($userId));
}
- public function scopeWithStageId(Builder $query, int $stageId): Builder
+ public function scopeWithStageIds(Builder $query, int|array $stageId): Builder
{
- return $query->where("stage_id", $stageId);
+ return $query->whereIn('stage_id', Arr::wrap($stageId));
}
}
diff --git a/locale/en/default.po b/locale/en/default.po
index 55b6e68c2ba..54c6194ac83 100644
--- a/locale/en/default.po
+++ b/locale/en/default.po
@@ -170,6 +170,15 @@ msgstr ""
"address. You can add information about this contributor in a message to the "
"editor at a later step in the submission process.
"
+msgid "default.submission.step.reviewerSuggestions"
+msgstr ""
+"When submitting your article, you have the option to suggest 2/3 potentials "
+"reviewers. This can help streampline the review process and provide valueable "
+"input for the editorial team. Please choose reviewers who are expert in your "
+"field and have not conflict of interest with your work. This feature amins to "
+"enhance the review process and and support a more efficient experience for both "
+"authors and journals.
"
+
msgid "default.submission.step.details"
msgstr ""
"Please provide the following details to help us manage your submission in "
diff --git a/locale/en/grid.po b/locale/en/grid.po
index f50a11b79f8..a7adad15b85 100644
--- a/locale/en/grid.po
+++ b/locale/en/grid.po
@@ -409,6 +409,17 @@ msgstr ""
"Are you sure you want to remove {$name} as a contributor? This action can "
"not be undone."
+msgid "grid.action.addReviewerSuggestion"
+msgstr "Add Reviewer Suggestion"
+
+msgid "grid.action.deleteReviewerSuggestion"
+msgstr "Delete Reviewer Suggestion"
+
+msgid "grid.action.deleteReviewerSuggestion.confirmationMessage"
+msgstr ""
+"Are you sure you want to remove this suggestion? This action can "
+"not be undone."
+
msgid "grid.action.newVersion"
msgstr "Create a new version"
diff --git a/locale/en/manager.po b/locale/en/manager.po
index 54e1e87cfb6..cb2da64be98 100644
--- a/locale/en/manager.po
+++ b/locale/en/manager.po
@@ -1327,6 +1327,9 @@ msgstr "The following is shown to authors during the upload files step. Provide
msgid "manager.setup.workflow.contributorsHelp.description"
msgstr "The following is shown to authors during the contributors step. Provide a brief explanation of what information the author should provide about themselves, co-authors, and any other contributors."
+msgid "manager.setup.workflow.reviewerSuggestionsHelp.description"
+msgstr "The following is shown to authors during the reviewer suggestions step. Provide a brief explanation of what information the author should provide about themselves, co-authors, and any other contributors."
+
msgid "manager.setup.workflow.detailsHelp.description"
msgstr "The following is shown to authors during the details step, when they are asked to provide the title, abstract, and other key information about their submission."
diff --git a/locale/en/submission.po b/locale/en/submission.po
index b9c2c65e84b..d856fe5c40f 100644
--- a/locale/en/submission.po
+++ b/locale/en/submission.po
@@ -663,6 +663,9 @@ msgstr "First published"
msgid "submission.forTheEditors"
msgstr "For the Editors"
+msgid "submission.forReviewerSuggestion"
+msgstr "For Reviewer Suggestion"
+
msgid "submission.galley"
msgstr "Galley"
@@ -1245,6 +1248,9 @@ msgstr "Upload a File Ready for Publication"
msgid "submission.upload.noAvailableReviewFiles"
msgstr "There are no files for you to revise at this time."
+msgid "submission.reviewerSuggestions"
+msgstr "Reviewer Suggestions"
+
msgid "editor.submission.roundStatus.revisionsRequested"
msgstr "Revisions have been requested."
@@ -2177,6 +2183,9 @@ msgstr "Make a Submission: {$step}"
msgid "submission.wizard.noContributors"
msgstr "No contributors have been added for this submission."
+msgid "submission.wizard.noReviewerSuggestions"
+msgstr "No reviewers have been suggested for this submission."
+
msgid "submission.wizard.missingContributorLanguage"
msgstr "The given name is missing in {$language} for one or more of the contributors."
@@ -2452,5 +2461,11 @@ msgstr "Competing Interests"
msgid "author.competingInterests.description"
msgstr "Please disclose any competing interests this author may have with the research subject."
+msgid "reviewerSuggestion.suggestionReason"
+msgstr "Reasons for suggesting reviewer"
+
+msgid "reviewerSuggestion.suggestionReason.description"
+msgstr "Please share why you are recommending this reviewer and metion is there are any potential conflict of interest."
+
msgid "submission.localeNotSupported"
msgstr "The language of the submission ({$language}) is not one of the supported submission languages. You can still edit the submission details but new submissions are not currently accepted with this language."
diff --git a/pages/submission/PKPSubmissionHandler.php b/pages/submission/PKPSubmissionHandler.php
index 3bc2b037e64..ec1f6945e71 100644
--- a/pages/submission/PKPSubmissionHandler.php
+++ b/pages/submission/PKPSubmissionHandler.php
@@ -34,6 +34,7 @@
use PKP\components\forms\submission\ForTheEditors;
use PKP\components\forms\submission\PKPSubmissionFileForm;
use PKP\components\listPanels\ContributorsListPanel;
+use PKP\components\listPanels\ReviewerSuggestionsListPanel;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\security\authorization\SubmissionAccessPolicy;
@@ -48,6 +49,7 @@ abstract class PKPSubmissionHandler extends Handler
{
public const SECTION_TYPE_CONFIRM = 'confirm';
public const SECTION_TYPE_CONTRIBUTORS = 'contributors';
+ public const SECTION_TYPE_REVIEWER_SUGGESTIONS = 'reviewerSuggestions';
public const SECTION_TYPE_FILES = 'files';
public const SECTION_TYPE_FORM = 'form';
public const SECTION_TYPE_TEMPLATE = 'template';
@@ -216,16 +218,21 @@ protected function showWizard(array $args, Request $request, Submission $submiss
$reconfigureSubmissionForm = $this->getReconfigureForm($context, $submission, $publication, $sections, $categories);
$steps = $this->getSteps($request, $submission, $publication, $formLocales, $sections, $categories);
+ $components = [
+ $submissionFilesListPanel['id'] => $submissionFilesListPanel,
+ $contributorsListPanel->id => $contributorsListPanel->getConfig(),
+ $reconfigureSubmissionForm->id => $reconfigureSubmissionForm->getConfig(),
+ ];
- $templateMgr = TemplateManager::getManager($request);
+ if ($context->getData('reviewerSuggestionEnabled')) {
+ $reviewerSuggestionsListPanel = $this->getReviewerSuggestionsListPanel($request, $submission, $publication, $formLocales);
+ $components[$reviewerSuggestionsListPanel->id] = $reviewerSuggestionsListPanel->getConfig();
+ }
+ $templateMgr = TemplateManager::getManager($request);
$templateMgr->setState([
'categories' => Repo::category()->getBreadcrumbs($categories),
- 'components' => [
- $submissionFilesListPanel['id'] => $submissionFilesListPanel,
- $contributorsListPanel->id => $contributorsListPanel->getConfig(),
- $reconfigureSubmissionForm->id => $reconfigureSubmissionForm->getConfig(),
- ],
+ 'components' => $components,
'i18nConfirmSubmit' => $this->getConfirmSubmitMessage($submission, $context),
'i18nDiscardChanges' => __('common.discardChanges'),
'i18nDisconnected' => __('common.disconnected'),
@@ -312,6 +319,11 @@ protected function getSteps(Request $request, Submission $submission, Publicatio
$steps[] = $this->getFilesStep($request, $submission, $publication, $locales, $publicationApiUrl);
$steps[] = $this->getContributorsStep($request, $submission, $publication, $locales, $publicationApiUrl);
$steps[] = $this->getEditorsStep($request, $submission, $publication, $locales, $publicationApiUrl, $categories);
+
+ if ($request->getContext()->getData('reviewerSuggestionEnabled')) {
+ $steps[] = $this->getReviewerSuggestionsStep($request);
+ }
+
$steps[] = $this->getConfirmStep($request, $submission, $publication, $locales, $publicationApiUrl);
return $steps;
@@ -471,7 +483,12 @@ protected function getSubmissionFilesListPanel(Request $request, Submission $sub
/**
* Get an instance of the ContributorsListPanel component
*/
- protected function getContributorsListPanel(Request $request, Submission $submission, Publication $publication, array $locales): ContributorsListPanel
+ protected function getContributorsListPanel(
+ Request $request,
+ Submission $submission,
+ Publication $publication,
+ array $locales
+ ): ContributorsListPanel
{
return new ContributorsListPanel(
'contributors',
@@ -484,6 +501,27 @@ protected function getContributorsListPanel(Request $request, Submission $submis
);
}
+ /**
+ * Get an instance of the ContributorsListPanel component
+ */
+ protected function getReviewerSuggestionsListPanel(
+ Request $request,
+ Submission $submission,
+ Publication $publication,
+ array $locales
+ ): ReviewerSuggestionsListPanel
+ {
+ return new ReviewerSuggestionsListPanel(
+ 'reviewerSuggestions',
+ __('submission.reviewerSuggestions'),
+ $submission,
+ $request->getContext(),
+ $locales,
+ [], // Populated by publication state
+ true
+ );
+ }
+
/**
* Get the user groups that a user can submit in
*/
@@ -534,7 +572,13 @@ protected function getFilesStep(Request $request, Submission $submission, Public
/**
* Get the state for the contributors step
*/
- protected function getContributorsStep(Request $request, Submission $submission, Publication $publication, array $locales, string $publicationApiUrl): array
+ protected function getContributorsStep(
+ Request $request,
+ Submission $submission,
+ Publication $publication,
+ array $locales,
+ string $publicationApiUrl
+ ): array
{
return [
'id' => 'contributors',
@@ -552,10 +596,39 @@ protected function getContributorsStep(Request $request, Submission $submission,
];
}
+ /**
+ * Get the state for the reviewer suggestion step
+ */
+ protected function getReviewerSuggestionsStep(Request $request): array
+ {
+ return [
+ 'id' => 'reviewerSuggestions',
+ 'name' => __('submission.reviewerSuggestions'),
+ 'reviewName' => __('submission.reviewerSuggestions'),
+ 'sections' => [
+ [
+ 'id' => 'reviewerSuggestions',
+ 'name' => __('submission.reviewerSuggestions'),
+ 'type' => self::SECTION_TYPE_REVIEWER_SUGGESTIONS,
+ 'description' => $request->getContext()->getLocalizedData('reviewerSuggestionsHelp'),
+ ],
+ ],
+ 'reviewTemplate' => '/submission/review-reviewer-suggestions.tpl',
+ ];
+ }
+
/**
* Get the state for the details step
*/
- protected function getDetailsStep(Request $request, Submission $submission, Publication $publication, array $locales, string $publicationApiUrl, array $sections, string $controlledVocabUrl): array
+ protected function getDetailsStep(
+ Request $request,
+ Submission $submission,
+ Publication $publication,
+ array $locales,
+ string $publicationApiUrl,
+ array $sections,
+ string $controlledVocabUrl
+ ): array
{
$titleAbstractForm = $this->getDetailsForm(
$publicationApiUrl,
@@ -608,7 +681,14 @@ protected function getDetailsStep(Request $request, Submission $submission, Publ
* If no metadata is enabled during submission, the metadata
* form is not shown.
*/
- protected function getEditorsStep(Request $request, Submission $submission, Publication $publication, array $locales, string $publicationApiUrl, LazyCollection $categories): array
+ protected function getEditorsStep(
+ Request $request,
+ Submission $submission,
+ Publication $publication,
+ array $locales,
+ string $publicationApiUrl,
+ LazyCollection $categories
+ ): array
{
$metadataForm = $this->getForTheEditorsForm(
$publicationApiUrl,
@@ -672,7 +752,13 @@ protected function getEditorsStep(Request $request, Submission $submission, Publ
/**
* Get the state for the Confirm step
*/
- protected function getConfirmStep(Request $request, Submission $submission, Publication $publication, array $locales, string $publicationApiUrl): array
+ protected function getConfirmStep(
+ Request $request,
+ Submission $submission,
+ Publication $publication,
+ array $locales,
+ string $publicationApiUrl
+ ): array
{
$sections = [
[
diff --git a/templates/submission/review-reviewer-suggestions.tpl b/templates/submission/review-reviewer-suggestions.tpl
new file mode 100644
index 00000000000..618a1179874
--- /dev/null
+++ b/templates/submission/review-reviewer-suggestions.tpl
@@ -0,0 +1,69 @@
+{**
+ * templates/submission/review-contributors.tpl
+ *
+ * Copyright (c) 2014-2022 Simon Fraser University
+ * Copyright (c) 2003-2022 John Willinsky
+ * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
+ *
+ * The template in the submission wizard when reviewing the contributors step.
+ *}
+
+
+
+
+
+
+ {translate key="submission.wizard.noReviewerSuggestions"}
+
+
+ -
+
+
+ {{ error }}
+
+
+ -
+
+ {{ localize(reviewerSuggestion.fullName) }}
+ {{ reviewerSuggestion.email }}
+
+
+ {{ localize(reviewerSuggestion.affiliation) }}
+
+
+ {call_hook name="Template::SubmissionWizard::Section::Review::ReviewerSuggestions" submission=$submission step=$step.id}
+
+
+
\ No newline at end of file
diff --git a/templates/submission/wizard.tpl b/templates/submission/wizard.tpl
index b1f62365197..7f41f1ab026 100644
--- a/templates/submission/wizard.tpl
+++ b/templates/submission/wizard.tpl
@@ -88,6 +88,14 @@
@updated:contributors="setContributors"
@updated:publication="setPublication"
>
+