Skip to content
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
121 changes: 121 additions & 0 deletions api/v1/vocabs/PKPInterestController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/**
* @file api/v1/vocabs/PKPInterestController.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 PKPInterestController
*
* @ingroup api_v1_vocabs
*
* @brief Controller class to handle API requests for user interests vocabulary.
* This is a public endpoint with no authentication required.
*/

namespace PKP\API\v1\vocabs;

use APP\core\Application;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use PKP\controlledVocab\ControlledVocabEntry;
use PKP\controlledVocab\ControlledVocabEntryMatch;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
use PKP\user\interest\UserInterest;

class PKPInterestController extends PKPBaseController
{
/**
* @copydoc \PKP\core\PKPBaseController::getHandlerPath()
*/
public function getHandlerPath(): string
{
return 'vocabs/interests';
}

/**
* @copydoc \PKP\core\PKPBaseController::getRouteGroupMiddleware()
*/
public function getRouteGroupMiddleware(): array
{
// No authentication required - publicly accessible endpoint
return [];
}

/**
* @copydoc \PKP\core\PKPBaseController::getGroupRoutes()
*/
public function getGroupRoutes(): void
{
Route::get('', $this->getMany(...))->name('interest.getMany');
}

/**
* @copydoc \PKP\core\PKPBaseController::authorize()
*/
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
{
// No authorization required for public endpoint
return true;
}

/**
* Get a collection of interests (controlled vocab entries)
*
* This endpoint returns site-wide user interests, which are shared across
* the entire application and not bound to any specific context.
*
* Interests are not multilingual and are stored with an empty locale.
*
* @param \Illuminate\Http\Request $illuminateRequest
*
* @return \Illuminate\Http\JsonResponse
*/
public function getMany(Request $illuminateRequest): JsonResponse
{
$requestParams = $illuminateRequest->query();

// Get the search term if provided
$term = $requestParams['term'] ?? null;

// Query controlled vocab entries for interests (site-wide, not multilingual)
$entries = ControlledVocabEntry::query()
->whereHas(
'controlledVocab',
fn ($query) => $query
->withSymbolics([UserInterest::CONTROLLED_VOCAB_INTEREST])
->withAssoc(Application::ASSOC_TYPE_SITE, Application::SITE_CONTEXT_ID)
)
->withLocales(['']) // Interests are stored with empty locale
->when(
$term,
fn ($query) => $query->withSetting('name', $term, ControlledVocabEntryMatch::PARTIAL)
)
->get();

// Transform entries to match the vocabs endpoint format
// For interests (stored with empty locale), extract string values from multilingual arrays
$data = collect($entries)
->map(function (ControlledVocabEntry $entry): array {
$entryData = $entry->getEntryData('');
// Flatten any multilingual properties by extracting the first value
return collect($entryData)
->map(fn ($value) => is_array($value) ? collect($value)->first() : $value)
->toArray();
})
->unique(fn (array $entryData): string =>
($entryData[ControlledVocabEntry::CONTROLLED_VOCAB_ENTRY_IDENTIFIER] ?? '') .
($entryData[ControlledVocabEntry::CONTROLLED_VOCAB_ENTRY_SOURCE] ?? '') .
($entryData['name'] ?? '')
)
->values()
->toArray();

return response()->json($data, Response::HTTP_OK);
}
}
12 changes: 10 additions & 2 deletions classes/template/PKPTemplateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -1984,6 +1984,7 @@ public function smartyRunHook(array $params): ?string
* - context
* - page
* - component
* - endpoint (for API routes)
* - op
* - path (array)
* - anchor
Expand All @@ -2009,8 +2010,8 @@ public function smartyUrl($parameters, $smarty = null): string
// Extract the reserved variables named in $paramList, and remove them
// from the parameters array. Variables remaining in parameters will be passed
// along to Request::url as extra parameters.
$params = $router = $page = $component = $anchor = $escape = $op = $path = $urlLocaleForPage = null;
$paramList = ['params', 'router', 'context', 'page', 'component', 'op', 'path', 'anchor', 'escape', 'urlLocaleForPage'];
$params = $router = $page = $component = $endpoint = $anchor = $escape = $op = $path = $urlLocaleForPage = null;
$paramList = ['params', 'router', 'context', 'page', 'component', 'endpoint', 'op', 'path', 'anchor', 'escape', 'urlLocaleForPage'];
foreach ($paramList as $parameter) {
if (isset($parameters[$parameter])) {
$$parameter = $parameters[$parameter];
Expand Down Expand Up @@ -2040,10 +2041,17 @@ public function smartyUrl($parameters, $smarty = null): string
$handler = match ($router) {
PKPApplication::ROUTE_PAGE => $page,
PKPApplication::ROUTE_COMPONENT => $component,
PKPApplication::ROUTE_API => $endpoint,
};

// Let the dispatcher create the url
$dispatcher = Application::get()->getDispatcher();

// API routes require context as string path
if ($router === PKPApplication::ROUTE_API && !is_string($context)) {
$context = ($this->_request->getContext())?->getPath() ?? Application::SITE_CONTEXT_PATH;
}

return $dispatcher->url($this->_request, $router, $context, $handler, $op, $path, $parameters, $anchor, !isset($escape) || $escape, $urlLocaleForPage);
}

Expand Down
19 changes: 0 additions & 19 deletions pages/user/PKPUserHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@
namespace PKP\pages\user;

use APP\core\Request;
use APP\facades\Repo;
use APP\handler\Handler;
use APP\template\TemplateManager;
use PKP\core\JSONMessage;
use PKP\core\PKPRequest;
use PKP\security\Validation;

class PKPUserHandler extends Handler
Expand All @@ -34,22 +31,6 @@ public function index($args, $request)
$request->redirect(null, null, 'profile');
}

/**
* Get interests for reviewer interests autocomplete.
*
* @param array $args
* @param PKPRequest $request
*
* @return JSONMessage JSON object
*/
public function getInterests($args, $request)
{
return new JSONMessage(
true,
Repo::userInterest()->getAllInterests($request->getUserVar('term'))
);
}

/**
* Display an authorization denied message.
*
Expand Down
10 changes: 6 additions & 4 deletions templates/form/interestsInput.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
autocomplete: {ldelim}
source: function(request, response) {ldelim}
$.ajax({ldelim}
url: {url|json_encode router=PKP\core\PKPApplication::ROUTE_PAGE page='user' op='getInterests' escape=false},
url: {url|json_encode router=PKP\core\PKPApplication::ROUTE_API endpoint='vocabs/interests' escape=false},
data: {ldelim}'term': request.term{rdelim},
dataType: 'json',
success: function(jsonData) {ldelim}
if (jsonData.status == true) {ldelim}
response(jsonData.content);
{rdelim}
// Extract interest names from the API response
var interests = jsonData.map(function(item) {ldelim}
return item.name;
{rdelim});
response(interests);
{rdelim}
{rdelim});
{rdelim}
Expand Down