Skip to content

Commit 4ba39aa

Browse files
Merge branch 'interestsFilter-3_3_0' into 'stable-3_3_0'
Adds filters in the search for reviewers and changes the plugin's display name. See merge request softwares-pkp/plugins_ojs/selectionOfReviewingInterests!18
2 parents da1af1a + 7f4511d commit 4ba39aa

10 files changed

Lines changed: 363 additions & 58 deletions

File tree

.github/workflows/generate-package.yml

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,8 @@ name: Create release and tar.gz package for it
77

88
jobs:
99
create-release:
10-
name: Create release and package
11-
env:
12-
PLUGIN_NAME: selectionOfReviewingInterests
13-
runs-on: ubuntu-latest
14-
steps:
15-
- name: Checkout code
16-
uses: actions/checkout@v2
17-
- name: Check version.xml
18-
run: |
19-
sudo apt install xmlstarlet
20-
application=$(xmlstarlet sel -t -v 'version/application' version.xml)
21-
if [ $application != $PLUGIN_NAME ]; then exit 1; fi
22-
release=$(xmlstarlet sel -t -v 'version/release' version.xml)
23-
tag=${{ github.ref }}
24-
tag=${tag/refs\/tags\/v}
25-
if [[ $release != $tag* ]]; then exit 1; fi
26-
date_version=$(xmlstarlet sel -t -v 'version/date' version.xml)
27-
current_date=$(date +'%Y-%m-%d')
28-
if [ $date_version != $current_date ]; then exit 1; fi
29-
shell: bash
30-
- name: Create the tar.gz package
31-
run: |
32-
mkdir $PLUGIN_NAME
33-
ls -la
34-
shopt -s extglob
35-
cp -r !($PLUGIN_NAME|.git*|.|..|tests|cypress|resources|CLAUDE.md|package.json|package-lock.json|vite.config.js|i18nExtractKeys.vite.js) $PLUGIN_NAME
36-
ls -la
37-
tar -zcvf $PLUGIN_NAME.tar.gz $PLUGIN_NAME
38-
ls -la
39-
shell: bash
40-
- name: Create the release
41-
id: create_release
42-
uses: actions/create-release@v1
43-
env:
44-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45-
with:
46-
tag_name: ${{ github.ref }}
47-
release_name: Release ${{ github.ref }}
48-
draft: false
49-
prerelease: false
50-
- name: Upload the package as release asset
51-
id: upload-release-asset
52-
uses: actions/upload-release-asset@v1
53-
env:
54-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55-
with:
56-
upload_url: ${{ steps.create_release.outputs.upload_url }}
57-
asset_path: ./${{ env.PLUGIN_NAME }}.tar.gz
58-
asset_name: ${{ env.PLUGIN_NAME }}.tar.gz
59-
asset_content_type: application/x-compressed-tar
10+
uses: lepidus/github-workflows/.github/workflows/generate-package.yml@main
11+
with:
12+
plugin_name: selectionOfReviewingInterests
13+
permissions:
14+
contents: write

SelectionOfReviewingInterestsPlugin.inc.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ public function register($category, $path, $mainContextId = null)
1313
if ($success && $this->getEnabled()) {
1414
$hookCallbacks = new HookCallbacks($this);
1515
HookRegistry::register('TemplateManager::display', [$hookCallbacks, 'addChangesOnTemplateDisplaying']);
16+
HookRegistry::register('advancedsearchreviewerform::display', [$hookCallbacks, 'addReviewerInterestFilterOnFormDisplay']);
1617
HookRegistry::register('Request::redirect', [$hookCallbacks, 'redirectUserAfterLogin']);
1718
HookRegistry::register('LoadComponentHandler', [$hookCallbacks, 'setupOptionsConfigurationGridHandler']);
19+
HookRegistry::register('API::users::reviewers::params', [$hookCallbacks, 'addReviewerInterestFilterParam']);
20+
HookRegistry::register('User::getReviewers::queryBuilder', [$hookCallbacks, 'setReviewerInterestFilter']);
21+
HookRegistry::register('User::getMany::queryObject', [$hookCallbacks, 'applyReviewerInterestFilter']);
1822
}
1923
return $success;
2024
}

classes/HookCallbacks.inc.php

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
class HookCallbacks
44
{
5+
private const REVIEWER_INTEREST_FILTER_PARAM_PREFIX = 'interestOption__';
6+
57
private $plugin;
68

79
public function __construct($plugin)
@@ -80,6 +82,23 @@ public function requestMessageFilter($output, $templateMgr)
8082
return $output;
8183
}
8284

85+
public function reviewerInterestFilterMarkupFilter($output, $templateMgr)
86+
{
87+
$pattern = '/<script type="text\/javascript">\s*pkp\.registry\.init\(\'select-reviewer-[^\']+\',\s*\'Container\',\s*\{.*?<\/script>/s';
88+
if (preg_match($pattern, $output, $matches, PREG_OFFSET_CAPTURE)) {
89+
$offset = $matches[0][1];
90+
91+
$newOutput = substr($output, 0, $offset);
92+
$newOutput .= $this->getReviewerInterestFilterComponentScriptTag();
93+
$newOutput .= substr($output, $offset);
94+
95+
$output = $newOutput;
96+
$templateMgr->unregisterFilter('output', [$this, 'reviewerInterestFilterMarkupFilter']);
97+
}
98+
99+
return $output;
100+
}
101+
83102
public function registrationInterestsFilter($output, $templateMgr)
84103
{
85104
$pattern = '/<div\s+id="reviewerInterests"[^>]*>.*?<\/div>/s';
@@ -101,10 +120,99 @@ public function setupOptionsConfigurationGridHandler(string $hookName, array $pa
101120
return false;
102121
}
103122

123+
public function addReviewerInterestFilterOnFormDisplay(string $hookName, array $params)
124+
{
125+
$request = Application::get()->getRequest();
126+
$context = $request->getContext();
127+
if (!$context) {
128+
return false;
129+
}
130+
131+
$templateMgr = TemplateManager::getManager($request);
132+
$this->addReviewerInterestFilter($templateMgr, $context->getId());
133+
134+
return false;
135+
}
136+
137+
public function addReviewerInterestFilterParam(string $hookName, array $params)
138+
{
139+
$reviewerParams = &$params[0];
140+
$slimRequest = $params[1];
141+
142+
$request = Application::get()->getRequest();
143+
$context = $request->getContext();
144+
if (!$context) {
145+
return false;
146+
}
147+
148+
$availableOptions = $this->getInterestOptions($context->getId());
149+
$selectedOptions = [];
150+
foreach ($slimRequest->getQueryParams() as $param => $value) {
151+
if (strpos($param, self::REVIEWER_INTEREST_FILTER_PARAM_PREFIX) !== 0) {
152+
continue;
153+
}
154+
155+
if (is_string($value)) {
156+
$selectedOptions[] = $value;
157+
}
158+
}
159+
160+
$validatedOptions = $this->normalizeSelectedInterestOptions($selectedOptions, $availableOptions);
161+
if (empty($validatedOptions)) {
162+
return false;
163+
}
164+
165+
$reviewerParams['interestOption'] = $validatedOptions;
166+
167+
return false;
168+
}
169+
170+
public function setReviewerInterestFilter(string $hookName, array $params)
171+
{
172+
$reviewerQueryBuilder = $params[0];
173+
$args = $params[1];
174+
175+
if (empty($args['interestOption']) || (!is_string($args['interestOption']) && !is_array($args['interestOption']))) {
176+
return false;
177+
}
178+
179+
$interestOptions = $this->normalizeSelectedInterestOptions($args['interestOption']);
180+
if (empty($interestOptions)) {
181+
return false;
182+
}
183+
184+
$reviewerQueryBuilder->selectionOfReviewingInterestsInterestOptions = $interestOptions;
185+
186+
return false;
187+
}
188+
189+
public function applyReviewerInterestFilter(string $hookName, array $params)
190+
{
191+
$query = $params[0];
192+
$userQueryBuilder = $params[1];
193+
194+
if (empty($userQueryBuilder->selectionOfReviewingInterestsInterestOptions)) {
195+
return false;
196+
}
197+
198+
$interestOptions = $userQueryBuilder->selectionOfReviewingInterestsInterestOptions;
199+
$normalizedInterestOptions = array_map('mb_strtolower', $interestOptions);
200+
$placeholders = implode(', ', array_fill(0, count($normalizedInterestOptions), '?'));
201+
202+
$query->whereExists(function ($query) use ($normalizedInterestOptions, $placeholders) {
203+
$query->from('user_interests', 'ui')
204+
->join('controlled_vocab_entry_settings AS cves', 'ui.controlled_vocab_entry_id', '=', 'cves.controlled_vocab_entry_id')
205+
->whereColumn('ui.user_id', '=', 'u.user_id')
206+
->where('cves.setting_name', '=', 'interest')
207+
->whereRaw('LOWER(cves.setting_value) IN (' . $placeholders . ')', $normalizedInterestOptions);
208+
});
209+
210+
return false;
211+
}
212+
104213
private function addInterestsScripts($templateMgr, $contextId)
105214
{
106-
$options = $this->plugin->getSetting($contextId, 'interestOptions') ?: array();
107-
$optionsArray = array_values($options);
215+
$optionsArray = $this->getInterestOptions($contextId);
108216

109217
$inlineScript = '$.pkp.plugins.generic = $.pkp.plugins.generic || {};';
110218
$inlineScript .= '$.pkp.plugins.generic.selectionOfReviewingInterests = ';
@@ -149,4 +257,82 @@ private function addRegistrationFilter($templateMgr)
149257
[$this, 'registrationInterestsFilter']
150258
);
151259
}
260+
261+
private function addReviewerInterestFilter($templateMgr, $contextId)
262+
{
263+
$selectReviewerListData = $templateMgr->getTemplateVars('selectReviewerListData');
264+
if (empty($selectReviewerListData['components']['selectReviewer'])) {
265+
return;
266+
}
267+
268+
$interestOptions = $this->getInterestOptions($contextId);
269+
if (empty($interestOptions)) {
270+
return;
271+
}
272+
273+
$this->addReviewerInterestFilterMarkup($templateMgr);
274+
275+
$filters = $selectReviewerListData['components']['selectReviewer']['filters'] ?? [];
276+
foreach ($interestOptions as $index => $interestOption) {
277+
$filters[] = [
278+
'param' => self::REVIEWER_INTEREST_FILTER_PARAM_PREFIX . $index,
279+
'value' => $interestOption,
280+
'title' => $interestOption,
281+
'groupTitle' => 'Filter by interest area',
282+
'showGroupTitle' => $index === 0,
283+
'filterType' => 'reviewer-interest-filter',
284+
];
285+
}
286+
287+
$selectReviewerListData['components']['selectReviewer']['filters'] = $filters;
288+
$templateMgr->assign('selectReviewerListData', $selectReviewerListData);
289+
}
290+
291+
private function addReviewerInterestFilterMarkup($templateMgr)
292+
{
293+
$templateMgr->registerFilter(
294+
'output',
295+
[$this, 'reviewerInterestFilterMarkupFilter']
296+
);
297+
}
298+
299+
private function getReviewerInterestFilterComponentScriptTag()
300+
{
301+
$request = Application::get()->getRequest();
302+
$scriptUrl = $request->getBaseUrl() . '/' . $this->plugin->getPluginPath() . '/js/reviewerInterestFilter.js';
303+
304+
return '<script type="text/javascript" src="' . $scriptUrl . '"></script>';
305+
}
306+
307+
private function getInterestOptions($contextId)
308+
{
309+
$options = $this->plugin->getSetting($contextId, 'interestOptions') ?: array();
310+
$options = array_map('trim', array_values($options));
311+
312+
return array_values(array_filter($options, function ($option) {
313+
return $option !== '';
314+
}));
315+
}
316+
317+
private function normalizeSelectedInterestOptions($interestOptions, $availableOptions = null)
318+
{
319+
if (is_string($interestOptions)) {
320+
$interestOptions = [$interestOptions];
321+
} elseif (!is_array($interestOptions)) {
322+
return [];
323+
}
324+
325+
$interestOptions = array_map('trim', $interestOptions);
326+
$interestOptions = array_values(array_unique(array_filter($interestOptions, function ($option) {
327+
return is_string($option) && $option !== '';
328+
})));
329+
330+
if ($availableOptions === null) {
331+
return $interestOptions;
332+
}
333+
334+
return array_values(array_filter($interestOptions, function ($option) use ($availableOptions) {
335+
return in_array($option, $availableOptions, true);
336+
}));
337+
}
152338
}

cypress/tests/Test0_pluginSetup.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('Enable Selection Of Reviewing Interests plugin', function () {
1717

1818
cy.get('tr#' + pluginRowId + ' a.show_extras').click();
1919
cy.get('a[id^=' + pluginRowId + '-settings-button]').click();
20-
cy.get('.pkp_modal_panel > :nth-child(1)').contains('Selection Field in Reviewing Interests Area');
20+
cy.get('.pkp_modal_panel > :nth-child(1)').contains('Selection of Review Interests');
2121
cy.waitJQuery();
2222
cy.get('a[id^=component-plugins-generic-selectionofreviewinginterests-controllers-grid-interestoptionsgrid-addOption-button-]').contains('Add option').click();
2323
cy.get('input[id^=optionName-]').should('be.visible');
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
describe('Filtering reviews by interests', function () {
2+
it('Accessing the interests filter', function () {
3+
cy.login('dbarnes', null, 'publicknowledge');
4+
cy.get('.listPanel__itemSubtitle:visible:contains("Finocchiaro: Arguments About Arguments")').first()
5+
.parent().parent().within(() => {
6+
cy.get('.pkpButton:contains("View")').click();
7+
});
8+
9+
cy.get('#ui-id-3').click();
10+
cy.get('a[id^=component-grid-users-reviewer-reviewergrid-addReviewer-button-]').click();
11+
cy.get('[description=""] > .listPanel > .listPanel__header > .pkpHeader > .pkpHeader__actions > .pkpButton').click();
12+
cy.get(':nth-child(7) > .pkpFilter > label > input').click();
13+
cy.contains('Adela Gallego');
14+
})
15+
});

0 commit comments

Comments
 (0)