Skip to content

Commit 0683cfe

Browse files
committed
#11779 added API endpoint to create task templates
1 parent cc84491 commit 0683cfe

File tree

2 files changed

+289
-14
lines changed

2 files changed

+289
-14
lines changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<?php
2+
3+
namespace PKP\API\v1\editTaskTemplates;
4+
5+
use APP\facades\Repo;
6+
use Illuminate\Http\JsonResponse;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Http\Response;
9+
use Illuminate\Support\Carbon;
10+
use Illuminate\Support\Facades\DB;
11+
use Illuminate\Support\Facades\Route;
12+
use Illuminate\Support\Facades\Validator;
13+
use Illuminate\Validation\Rule;
14+
use PKP\API\v1\submissions\resources\TaskResource;
15+
use PKP\core\PKPBaseController;
16+
use PKP\core\PKPRequest;
17+
use PKP\editorialTask\EditorialTask;
18+
use PKP\editorialTask\enums\EditorialTaskType;
19+
use PKP\editorialTask\Participant;
20+
use PKP\security\authorization\CanAccessSettingsPolicy;
21+
use PKP\security\authorization\ContextAccessPolicy;
22+
use PKP\security\Role;
23+
use PKP\userGroup\UserGroup;
24+
use PKP\editorialTask\Template;
25+
26+
class PKPEditTaskTemplateController extends PKPBaseController
27+
{
28+
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
29+
{
30+
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
31+
$this->addPolicy(new CanAccessSettingsPolicy());
32+
return parent::authorize($request, $args, $roleAssignments);
33+
}
34+
35+
public function getHandlerPath(): string
36+
{
37+
return 'editTaskTemplates';
38+
}
39+
40+
public function getRouteGroupMiddleware(): array
41+
{
42+
return ['has.user', 'has.context'];
43+
}
44+
45+
public function getGroupRoutes(): void
46+
{
47+
Route::middleware([
48+
self::roleAuthorizer([
49+
Role::ROLE_ID_MANAGER,
50+
Role::ROLE_ID_SITE_ADMIN,
51+
]),
52+
])->group(function () {
53+
Route::post('', $this->add(...));
54+
});
55+
}
56+
57+
/**
58+
* POST /api/v1/editTaskTemplates
59+
* Body: stageId, title, include, type, dateDue?, userGroupIds[], participants[] (userId,isResponsible?)
60+
*/
61+
public function add(Request $illuminateRequest): JsonResponse
62+
{
63+
$request = $this->getRequest();
64+
$context = $request->getContext();
65+
$currentUser = $request->getUser();
66+
67+
// gather payload
68+
$payload = [
69+
'stageId' => (int) $illuminateRequest->input('stageId'),
70+
'title' => (string) $illuminateRequest->input('title', ''),
71+
'include' => (bool) $illuminateRequest->boolean('include', false),
72+
'type' => (int) $illuminateRequest->input('type'), // see #2
73+
'emailTemplateId' => $illuminateRequest->input('emailTemplateId'),
74+
'userGroupIds' => array_values(array_map('intval', (array) $illuminateRequest->input('userGroupIds', []))),
75+
'participants' => (array) $illuminateRequest->input('participants', []),
76+
];
77+
78+
if ($illuminateRequest->has('dateDue')) {
79+
$payload['dateDue'] = $illuminateRequest->input('dateDue');
80+
}
81+
82+
// validation
83+
$typeValues = array_column(EditorialTaskType::cases(), 'value');
84+
$isTask = $payload['type'] === EditorialTaskType::TASK->value;
85+
$isDiscussion = $payload['type'] === EditorialTaskType::DISCUSSION->value;
86+
87+
$rules = [
88+
'stageId' => ['required', 'integer', 'min:1'],
89+
'title' => ['required', 'string', 'max:255'],
90+
'include' => ['boolean'],
91+
92+
'type' => ['required', Rule::in($typeValues)],
93+
'dateDue' => $isTask
94+
? ['required', 'date_format:Y-m-d', 'after:today']
95+
: ['prohibited'],
96+
97+
'emailTemplateId' => ['nullable', 'integer', Rule::exists('email_templates', 'email_id')],
98+
99+
'userGroupIds' => ['required', 'array', 'min:1'],
100+
'userGroupIds.*' => ['integer', 'distinct', Rule::exists('user_groups', 'user_group_id')],
101+
102+
'participants' => ['required', 'array'],
103+
'participants.*' => ['required', 'array:userId,isResponsible'],
104+
'participants.*.userId' => ['required', 'integer', 'distinct', Rule::exists('users', 'user_id')],
105+
'participants.*.isResponsible' => $isTask ? ['required', 'boolean'] : ['prohibited'],
106+
];
107+
108+
$validator = Validator::make($payload, $rules);
109+
110+
$validator->after(function ($v) use ($payload, $isTask, $isDiscussion) {
111+
$parts = $payload['participants'] ?? [];
112+
113+
if ($isTask && count($parts) < 1) {
114+
$v->errors()->add('participants', 'At least one participant is required for a task.');
115+
}
116+
if ($isDiscussion && count($parts) < 2) {
117+
$v->errors()->add('participants', 'At least two participants are required for a discussion.');
118+
}
119+
120+
if ($isTask) {
121+
$responsibles = 0;
122+
foreach ($parts as $p) {
123+
if (!empty($p['isResponsible'])) {
124+
$responsibles++;
125+
}
126+
}
127+
if ($responsibles > 1) {
128+
$v->errors()->add('participants', 'There should be the only one user responsible for the task');
129+
}
130+
}
131+
});
132+
133+
if ($validator->fails()) {
134+
return response()->json($validator->errors(), Response::HTTP_UNPROCESSABLE_ENTITY);
135+
}
136+
137+
// userGroupIds must belong to this context
138+
$validCount = DB::table('user_groups')
139+
->where('context_id', $context->getId())
140+
->whereIn('user_group_id', $payload['userGroupIds'])
141+
->count();
142+
143+
if ($validCount !== count($payload['userGroupIds'])) {
144+
return response()->json(
145+
['userGroupIds' => ['One or more userGroupIds do not belong to this context']],
146+
Response::HTTP_BAD_REQUEST
147+
);
148+
}
149+
150+
$templateId = DB::transaction(function () use ($context, $payload) {
151+
$template = Template::create([
152+
'stage_id' => $payload['stageId'],
153+
'title' => $payload['title'],
154+
'context_id' => $context->getId(),
155+
'include' => $payload['include'],
156+
'email_template_id' => $payload['emailTemplateId'] ?? null,
157+
]);
158+
159+
// attach user groups via pivot
160+
$template->userGroups()->sync($payload['userGroupIds']);
161+
162+
// store extra template config in settings (non-localized)
163+
DB::table('edit_task_template_settings')->insert([
164+
[
165+
'edit_task_template_id' => $template->getKey(),
166+
'locale' => '',
167+
'setting_name' => 'type',
168+
'setting_value'=> (string) ((int) $payload['type']),
169+
],
170+
[
171+
'edit_task_template_id' => $template->getKey(),
172+
'locale' => '',
173+
'setting_name' => 'participants',
174+
'setting_value'=> json_encode(array_map(
175+
fn (array $p) => [
176+
'userId' => (int) $p['userId'],
177+
'isResponsible' => (bool) ($p['isResponsible'] ?? false)
178+
],
179+
$payload['participants']
180+
)),
181+
],
182+
[
183+
'edit_task_template_id' => $template->getKey(),
184+
'locale' => '',
185+
'setting_name' => 'dateDue',
186+
'setting_value'=> $payload['dateDue'] ? (string) $payload['dateDue'] : null,
187+
],
188+
]);
189+
190+
return $template->getKey();
191+
});
192+
193+
// serialize with TaskResource
194+
$tpl = DB::table('edit_task_templates')->where('edit_task_template_id', $templateId)->first();
195+
196+
$settings = DB::table('edit_task_template_settings')
197+
->where('edit_task_template_id', $templateId)
198+
->pluck('setting_value', 'setting_name');
199+
200+
$type = (int) ($settings['type'] ?? $payload['type']);
201+
$dateDueStr = $settings['dateDue'] ?? ($payload['dateDue'] ?? null);
202+
$dateDue = !empty($dateDueStr) ? Carbon::createFromFormat('Y-m-d', $dateDueStr) : null;
203+
204+
205+
$participantsSetting = $settings['participants'] ?? '[]';
206+
$participantsData = json_decode($participantsSetting, true) ?: [];
207+
208+
// Build a transient EditorialTask that mirrors a real task
209+
$editorialTask = new EditorialTask([
210+
'edit_task_id' => (int) $templateId,
211+
'type' => $type,
212+
'assocType' => null,
213+
'assocId' => null,
214+
'stageId' => (int) $tpl->stage_id,
215+
'title' => $tpl->title,
216+
'createdBy' => $currentUser->getId(),
217+
'dateDue' => $dateDue,
218+
'dateStarted'=> null,
219+
'dateClosed' => null,
220+
]);
221+
222+
// attach participants relation for TaskResource
223+
$participantModels = collect($participantsData)->map(
224+
fn (array $p) => new Participant([
225+
'userId' => (int) $p['userId'],
226+
'isResponsible' => (bool) ($p['isResponsible'] ?? false),
227+
])
228+
);
229+
$editorialTask->setRelation('participants', $participantModels);
230+
231+
// build the data bundle required by TaskResource
232+
$participantIds = $participantModels->pluck('userId')->unique()->values()->all();
233+
if (!in_array($currentUser->getId(), $participantIds, true)) {
234+
$participantIds[] = $currentUser->getId();
235+
}
236+
237+
$users = Repo::user()->getCollector()
238+
->filterByUserIds($participantIds)
239+
->getMany();
240+
241+
$userGroups = UserGroup::with('userUserGroups')
242+
->withContextIds($context->getId())
243+
->withUserIds($participantIds)
244+
->get();
245+
246+
return (new TaskResource(
247+
resource: $editorialTask,
248+
data: [
249+
'submission' => null,
250+
'users' => $users,
251+
'userGroups' => $userGroups,
252+
'stageAssignments' => collect(),
253+
'reviewAssignments' => collect(),
254+
]
255+
))->response()->setStatusCode(Response::HTTP_CREATED);
256+
257+
}
258+
}

classes/editorialTask/Template.php

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use Illuminate\Database\Eloquent\Casts\Attribute;
2020
use Illuminate\Database\Eloquent\Model;
21+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
2122
use PKP\core\traits\ModelWithSettings;
2223
use PKP\emailTemplate\EmailTemplate;
2324

@@ -28,28 +29,31 @@ class Template extends Model
2829
protected $table = 'edit_task_templates';
2930
protected $primaryKey = 'edit_task_template_id';
3031

31-
protected $guarded = [
32-
'id',
33-
'editTaskTemplateId'
32+
public $timestamps = true;
33+
34+
// columns on edit_task_templates
35+
protected $fillable = [
36+
'stage_id',
37+
'title',
38+
'context_id',
39+
'include',
40+
'email_template_id',
3441
];
3542

36-
protected function casts()
37-
{
38-
return [
39-
'id' => 'integer',
40-
'editTaskTemplateId' => 'integer',
41-
'emailTemplateId' => 'integer',
42-
'name' => 'string',
43-
'description' => 'string',
44-
];
45-
}
43+
protected $casts = [
44+
'stage_id' => 'int',
45+
'context_id' => 'int',
46+
'include' => 'bool',
47+
'email_template_id' => 'int',
48+
'title' => 'string',
49+
];
4650

4751
/**
4852
* @inheritDoc
4953
*/
5054
public function getSettingsTable(): string
5155
{
52-
return 'edit_task_settings';
56+
return 'edit_task_template_settings';
5357
}
5458

5559
/**
@@ -93,4 +97,17 @@ public function emailTemplate()
9397
{
9498
return $this->hasOne(EmailTemplate::class, 'email_id', 'email_template_id');
9599
}
100+
101+
/**
102+
* Link template to user groups via pivot table.
103+
*/
104+
public function userGroups(): BelongsToMany
105+
{
106+
return $this->belongsToMany(
107+
\PKP\userGroup\UserGroup::class,
108+
'edit_task_template_user_groups',
109+
'edit_task_template_id',
110+
'user_group_id'
111+
);
112+
}
96113
}

0 commit comments

Comments
 (0)