Skip to content

Commit b405119

Browse files
committed
#12392 Support Funder Data
1 parent 677b737 commit b405119

File tree

28 files changed

+1396
-1
lines changed

28 files changed

+1396
-1
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
<?php
2+
3+
/**
4+
* @file api/v1/funders/PKPFunderController.php
5+
*
6+
* Copyright (c) 2026 Simon Fraser University
7+
* Copyright (c) 2026 John Willinsky
8+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
9+
*
10+
* @class PKPFunderController
11+
*
12+
* @ingroup api_v1_funders
13+
*
14+
* @brief Controller class to handle API requests for funder operations.
15+
*
16+
*/
17+
18+
namespace pkp\api\v1\funders;
19+
20+
use APP\core\Application;
21+
use APP\facades\Repo;
22+
use Illuminate\Http\JsonResponse;
23+
use Illuminate\Http\Request;
24+
use Illuminate\Http\Response;
25+
use Illuminate\Support\Facades\Route;
26+
use PKP\funder\Funder;
27+
use PKP\core\PKPBaseController;
28+
use PKP\core\PKPRequest;
29+
use PKP\plugins\Hook;
30+
use PKP\security\authorization\ContextAccessPolicy;
31+
use PKP\security\authorization\PublicationAccessPolicy;
32+
use PKP\security\authorization\PublicationWritePolicy;
33+
use PKP\security\authorization\SubmissionAccessPolicy;
34+
use PKP\security\authorization\UserRolesRequiredPolicy;
35+
use PKP\security\Role;
36+
use PKP\services\PKPSchemaService;
37+
38+
class PKPFunderController extends PKPBaseController
39+
{
40+
/**
41+
* @copydoc \PKP\core\PKPBaseController::getHandlerPath()
42+
*/
43+
public function getHandlerPath(): string
44+
{
45+
return 'submissions/{submissionId}/publications/{publicationId}/funders';
46+
}
47+
48+
/**
49+
* @copydoc \PKP\core\PKPBaseController::getRouteGroupMiddleware()
50+
*/
51+
public function getRouteGroupMiddleware(): array
52+
{
53+
return [
54+
'has.user',
55+
'has.context',
56+
];
57+
}
58+
59+
/**
60+
* @copydoc \PKP\core\PKPBaseController::getGroupRoutes()
61+
*/
62+
public function getGroupRoutes(): void
63+
{
64+
65+
Route::middleware([
66+
self::roleAuthorizer([
67+
Role::ROLE_ID_MANAGER,
68+
Role::ROLE_ID_SITE_ADMIN,
69+
Role::ROLE_ID_SUB_EDITOR,
70+
Role::ROLE_ID_ASSISTANT,
71+
Role::ROLE_ID_AUTHOR,
72+
]),
73+
])->group(function () {
74+
75+
Route::get('', $this->getMany(...))
76+
->name('funders.getMany');
77+
78+
Route::get('{funderId}', $this->get(...))
79+
->name('funders.getFunder')
80+
->whereNumber('funderId');
81+
82+
Route::post('', $this->add(...))
83+
->name('funders.add');
84+
85+
Route::put('{funderId}', $this->edit(...))
86+
->name('funders.edit')
87+
->whereNumber('funderId');
88+
89+
Route::delete('{funderId}', $this->delete(...))
90+
->name('funders.delete')
91+
->whereNumber('funderId');
92+
93+
Route::put('order', $this->saveOrder(...))
94+
->name('funders.order');
95+
96+
})->whereNumber(['submissionId', 'publicationId']);
97+
}
98+
99+
/**
100+
* @copydoc \PKP\core\PKPBaseController::authorize()
101+
*/
102+
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
103+
{
104+
105+
$illuminateRequest = $args[0]; /** @var \Illuminate\Http\Request $illuminateRequest */
106+
$actionName = static::getRouteActionName($illuminateRequest);
107+
108+
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
109+
110+
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
111+
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
112+
113+
if (in_array($actionName, ['get', 'getMany'], true)) {
114+
$this->addPolicy(new PublicationAccessPolicy($request, $args, $roleAssignments));
115+
} else {
116+
$this->addPolicy(new PublicationWritePolicy($request, $args, $roleAssignments));
117+
}
118+
119+
return parent::authorize($request, $args, $roleAssignments);
120+
}
121+
122+
/**
123+
* Get a single funder.
124+
*/
125+
public function get(Request $illuminateRequest): JsonResponse
126+
{
127+
$funder = Funder::find((int) $illuminateRequest->route('funderId'));
128+
129+
130+
if (!$funder) {
131+
return response()->json([
132+
'error' => __('api.funders.404.funderNotFound')
133+
], Response::HTTP_NOT_FOUND);
134+
}
135+
136+
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
137+
138+
if ($submission->getId() !== $funder->submissionId) {
139+
return response()->json([
140+
'error' => __('api.funders.400.submissionsNotMatched'),
141+
], Response::HTTP_FORBIDDEN);
142+
}
143+
144+
return response()->json(Repo::funder()->getSchemaMap()->summarize($funder), Response::HTTP_OK);
145+
}
146+
147+
/**
148+
* Get a collection of funders.
149+
*
150+
* @hook API::funders::params [[$collector, $illuminateRequest]]
151+
*/
152+
public function getMany(Request $illuminateRequest): JsonResponse
153+
{
154+
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
155+
$funders = Funder::withSubmissionId($submission->getId())->orderBySeq();
156+
157+
Hook::run('API::funders::params', [$funders, $illuminateRequest]);
158+
159+
return response()->json([
160+
'itemsMax' => $funders->count(),
161+
'items' => Repo::funder()->getSchemaMap()->summarizeMany($funders->get())->values(),
162+
], Response::HTTP_OK);
163+
}
164+
165+
/**
166+
* Add a funder.
167+
*/
168+
public function add(Request $illuminateRequest): JsonResponse
169+
{
170+
$input = $illuminateRequest->input();
171+
172+
$ror = $input['funder']['ror'] ?? null;
173+
$params = [
174+
'ror' => $ror,
175+
'name' => $ror ? null : ($input['funder']['name'] ?? []),
176+
'grants' => $input['grants'] ?? [],
177+
'seq' => 0,
178+
];
179+
180+
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_FUNDER, $params);
181+
$readOnlyErrors = $this->getWriteDisabledErrors(PKPSchemaService::SCHEMA_FUNDER, $params);
182+
if ($readOnlyErrors) {
183+
return response()->json($readOnlyErrors, Response::HTTP_BAD_REQUEST);
184+
}
185+
186+
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
187+
$params['submissionId'] = (int) $submission->getId();
188+
189+
$errors = Repo::funder()->validate(null, $params);
190+
if (!empty($errors)) {
191+
return response()->json($errors, Response::HTTP_BAD_REQUEST);
192+
}
193+
194+
$funder = Funder::create($params);
195+
196+
return response()->json(Repo::funder()->getSchemaMap()->map($funder), Response::HTTP_OK);
197+
}
198+
199+
/**
200+
* Edit a funder.
201+
*/
202+
public function edit(Request $illuminateRequest): JsonResponse
203+
{
204+
$funder = Funder::find((int)$illuminateRequest->route('funderId'));
205+
206+
if (!$funder) {
207+
return response()->json([
208+
'error' => __('api.funders.404.funderNotFound'),
209+
], Response::HTTP_NOT_FOUND);
210+
}
211+
212+
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
213+
214+
if ($submission->getId() !== $funder->submissionId) {
215+
return response()->json([
216+
'error' => __('api.funders.400.submissionsNotMatched'),
217+
], Response::HTTP_FORBIDDEN);
218+
}
219+
220+
$input = $illuminateRequest->input();
221+
222+
$ror = $input['funder']['ror'] ?? null;
223+
$params = [
224+
'ror' => $ror,
225+
'name' => $ror ? null : ($input['funder']['name'] ?? []),
226+
'grants' => $input['grants'] ?? [],
227+
'seq' => 0,
228+
];
229+
230+
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_FUNDER, $params);
231+
232+
$readOnlyErrors = $this->getWriteDisabledErrors(PKPSchemaService::SCHEMA_FUNDER, $params);
233+
if (!empty($readOnlyErrors)) {
234+
return response()->json($readOnlyErrors, Response::HTTP_BAD_REQUEST);
235+
}
236+
237+
$params['id'] = $funder->id;
238+
239+
$errors = Repo::funder()->validate($funder, $params);
240+
if (!empty($errors)) {
241+
return response()->json($errors, Response::HTTP_BAD_REQUEST);
242+
}
243+
244+
$funder->update($params);
245+
246+
$funder = Funder::find($funder->id);
247+
248+
return response()->json(
249+
Repo::funder()->getSchemaMap()->map($funder), Response::HTTP_OK
250+
);
251+
}
252+
253+
/**
254+
* Delete a funder.
255+
*/
256+
public function delete(Request $illuminateRequest): JsonResponse
257+
{
258+
$funder = Funder::find((int) $illuminateRequest->route('funderId'));
259+
260+
if (!$funder) {
261+
return response()->json([
262+
'error' => __('api.funders.404.funderNotFound')
263+
], Response::HTTP_NOT_FOUND);
264+
}
265+
266+
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
267+
268+
if ($submission->getId() !== $funder->submissionId) {
269+
return response()->json([
270+
'error' => __('api.funders.400.submissionsNotMatched'),
271+
], Response::HTTP_FORBIDDEN);
272+
}
273+
274+
$funder->delete();
275+
276+
return response()->json(
277+
Repo::funder()->getSchemaMap()->map($funder), Response::HTTP_OK
278+
);
279+
}
280+
281+
/**
282+
* Save the order of funders for a publication.
283+
*/
284+
public function saveOrder(Request $illuminateRequest): JsonResponse
285+
{
286+
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
287+
288+
$submissionId = (int) $submission->getId();
289+
$sequence = $illuminateRequest->json()->all();
290+
291+
if (!is_array($sequence)) {
292+
return response()->json(
293+
['error' => __('api.funders.404.invalidOrderFormat')],
294+
Response::HTTP_BAD_REQUEST
295+
);
296+
}
297+
298+
foreach ($sequence as $index => $funderId) {
299+
Funder::where('funder_id', (int) $funderId)
300+
->where('submission_id', $submissionId)
301+
->update(['seq' => $index + 1]);
302+
}
303+
304+
return response()->json(['status' => true], Response::HTTP_OK);
305+
}
306+
307+
/**
308+
* This method returns errors for any params that match
309+
* properties in the schema with writeDisabledInApi set to true.
310+
*
311+
* This is used for properties that can not be edited through
312+
* the API, but which otherwise can be edited by the entity's
313+
* repository.
314+
*/
315+
protected function getWriteDisabledErrors(string $schemaName, array $params): array
316+
{
317+
$schema = app()->get('schema')->get($schemaName);
318+
319+
$writeDisabledProps = [];
320+
foreach ($schema->properties as $propName => $propSchema) {
321+
if (!empty($propSchema->writeDisabledInApi)) {
322+
$writeDisabledProps[] = $propName;
323+
}
324+
}
325+
326+
$errors = [];
327+
328+
$notAllowedProps = array_intersect(
329+
$writeDisabledProps,
330+
array_keys($params)
331+
);
332+
333+
if (!empty($notAllowedProps)) {
334+
foreach ($notAllowedProps as $propName) {
335+
$errors[$propName] = [__('api.400.propReadOnly', ['prop' => $propName])];
336+
}
337+
}
338+
339+
return $errors;
340+
}
341+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/**
4+
* @file classes/components/form/FieldFunder.php
5+
*
6+
* Copyright (c) 2026 Simon Fraser University
7+
* Copyright (c) 2026 John Willinsky
8+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
9+
*
10+
* @class FieldFunder
11+
*
12+
* @ingroup classes_controllers_form
13+
*
14+
* @brief A field for funder information.
15+
*/
16+
17+
namespace PKP\components\forms;
18+
19+
class FieldFunder extends Field
20+
{
21+
/** @copydoc Field::$component */
22+
public $component = 'field-funder';
23+
24+
/** @copydoc Field::$component */
25+
public $default = [];
26+
27+
/**
28+
* Submission ID associated with the funder
29+
*/
30+
public int $submissionId = 0;
31+
32+
/**
33+
* Primary language
34+
*/
35+
public string $primaryLocale = '';
36+
37+
/**
38+
* Supported locales for forms
39+
*/
40+
public array $supportedFormLocales = [];
41+
42+
/**
43+
* @copydoc Field::getConfig()
44+
*/
45+
public function getConfig()
46+
{
47+
$config = parent::getConfig();
48+
49+
$config['submissionId'] = $this->submissionId;
50+
$config['value'] = $this->value ?? $this->default ?? null;
51+
52+
return $config;
53+
}
54+
}

0 commit comments

Comments
 (0)