Skip to content

Commit 8b2614a

Browse files
committed
#11885 Implemented edit, delete, and search endpoints for task templates and fixed stageId on add
1 parent 01662d4 commit 8b2614a

File tree

7 files changed

+304
-7
lines changed

7 files changed

+304
-7
lines changed

api/v1/editTaskTemplates/PKPEditTaskTemplateController.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PKP\security\authorization\ContextAccessPolicy;
3030
use PKP\security\Role;
3131
use PKP\editorialTask\Template;
32+
use PKP\API\v1\editTaskTemplates\formRequests\UpdateTaskTemplate;
3233

3334
class PKPEditTaskTemplateController extends PKPBaseController
3435
{
@@ -59,6 +60,8 @@ public function getGroupRoutes(): void
5960
])->group(function () {
6061
Route::post('', $this->add(...));
6162
Route::get('', $this->getMany(...));
63+
Route::put('{templateId}', $this->update(...))->whereNumber('templateId');
64+
Route::delete('{templateId}', $this->delete(...))->whereNumber('templateId');
6265
});
6366
}
6467

@@ -91,6 +94,7 @@ public function add(AddTaskTemplate $illuminateRequest): JsonResponse
9194
'context_id' => $context->getId(),
9295
'include' => $validated['include'] ?? false,
9396
'email_template_key' => $validated['emailTemplateKey'] ?? null,
97+
'type' => (int) $validated['type'],
9498
]);
9599

96100
$tpl->userGroups()->sync($validated['userGroupIds']);
@@ -118,6 +122,9 @@ public function getMany(Request $request): JsonResponse
118122
->with('userGroups');
119123

120124
$queryParams = $this->_processAllowedParams($request->query(), [
125+
'search',
126+
'title',
127+
'type',
121128
'stageId',
122129
'include',
123130
'emailTemplateKey',
@@ -127,6 +134,20 @@ public function getMany(Request $request): JsonResponse
127134

128135
foreach ($queryParams as $param => $val) {
129136
switch ($param) {
137+
case 'search':
138+
$collector->filterBySearch((string) $val);
139+
break;
140+
case 'title':
141+
$collector->filterByTitleLike((string) $val);
142+
break;
143+
case 'type':
144+
if (is_numeric($val)) {
145+
$type = (int) $val;
146+
if ($type === Template::TYPE_DISCUSSION || $type === Template::TYPE_TASK) {
147+
$collector->filterByType($type);
148+
}
149+
}
150+
break;
130151
case 'stageId':
131152
$collector->filterByStageId((int) $val);
132153
break;
@@ -162,5 +183,67 @@ public function getMany(Request $request): JsonResponse
162183
->setStatusCode(Response::HTTP_OK);
163184
}
164185

186+
/**
187+
* UPDATE /api/v1/editTaskTemplates/{templateId}
188+
*/
189+
public function update(UpdateTaskTemplate $request): JsonResponse
190+
{
191+
$contextId = (int) $this->getRequest()->getContext()->getId();
192+
$id = (int) $request->route('templateId');
193+
194+
$template = Template::query()
195+
->where('context_id', $contextId)
196+
->findOrFail($id);
197+
198+
$data = $request->validated();
199+
200+
DB::transaction(function () use ($template, $data) {
201+
$updates = [];
202+
if (array_key_exists('stageId', $data)) $updates['stage_id'] = (int) $data['stageId'];
203+
if (array_key_exists('title', $data)) $updates['title'] = $data['title'];
204+
if (array_key_exists('include', $data)) $updates['include'] = (bool) $data['include'];
205+
if (array_key_exists('emailTemplateKey', $data)) $updates['email_template_key'] = $data['emailTemplateKey'];
206+
if (array_key_exists('type', $data)) $updates['type'] = (int) $data['type'];
207+
208+
if ($updates) {
209+
$template->update($updates);
210+
}
211+
if (array_key_exists('userGroupIds', $data)) {
212+
$template->userGroups()->sync($data['userGroupIds']);
213+
}
214+
});
215+
216+
return response()->json(
217+
(new TaskTemplateResource($template->refresh()->load('userGroups')))->toArray($request),
218+
Response::HTTP_OK
219+
);
220+
}
221+
222+
/**
223+
* DELETE /api/v1/editTaskTemplates/{templateId}
224+
*/
225+
public function delete(Request $illuminateRequest): JsonResponse
226+
{
227+
$contextId = (int) $this->getRequest()->getContext()->getId();
228+
$id = (int) $illuminateRequest->route('templateId');
229+
230+
$template = Template::query()
231+
->where('context_id', $contextId)
232+
->find($id);
233+
234+
if (!$template) {
235+
return response()->json([
236+
'error' => __('api.404.resourceNotFound'),
237+
], Response::HTTP_NOT_FOUND);
238+
}
239+
240+
DB::transaction(function () use ($template) {
241+
// Pivot/settings rows cascade via FKs defined in migration
242+
$template->delete();
243+
});
244+
245+
return response()->json([], Response::HTTP_OK);
246+
}
247+
165248
}
166249

api/v1/editTaskTemplates/formRequests/AddTaskTemplate.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,14 @@ public function authorize(): bool
3030
public function rules(): array
3131
{
3232
$contextId = Application::get()->getRequest()->getContext()->getId();
33-
$stageIds = array_keys(Application::getApplicationStages());
33+
34+
// build a list of allowed stage IDs from values
35+
$stages = Application::getApplicationStages();
36+
$stageIds = array_values(array_unique(array_filter(
37+
array_map('intval', array_merge(array_keys((array)$stages), array_values((array)$stages))),
38+
fn ($id) => $id > 0
39+
)));
40+
3441
$emailKeys = Repo::emailTemplate()
3542
->getCollector($contextId)
3643
->getMany()
@@ -43,6 +50,7 @@ public function rules(): array
4350
'title' => ['required', 'string', 'max:255'],
4451
'include' => ['boolean'],
4552
'emailTemplateKey' => ['sometimes', 'nullable', 'string', 'max:255', Rule::in($emailKeys)],
53+
'type' => ['required','integer','in:1,2'],
4654
'userGroupIds' => ['required', 'array', 'min:1'],
4755
'userGroupIds.*' => [
4856
'integer',
@@ -56,10 +64,12 @@ public function rules(): array
5664
protected function prepareForValidation(): void
5765
{
5866
$stageId = $this->input('stageId', null);
67+
$type = $this->input('type', null);
5968
$this->merge([
6069
'include' => filter_var($this->input('include', false), FILTER_VALIDATE_BOOLEAN),
6170
'userGroupIds' => array_values(array_map('intval', (array) $this->input('userGroupIds', []))),
6271
'stageId' => is_null($stageId) ? $stageId : (int) $stageId,
72+
'type' => is_null($type) ? null : (int) $type,
6373
]);
6474
}
6575

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace PKP\API\v1\editTaskTemplates\formRequests;
4+
5+
use APP\core\Application;
6+
use Illuminate\Foundation\Http\FormRequest;
7+
use Illuminate\Validation\Rule;
8+
9+
class UpdateTaskTemplate extends FormRequest
10+
{
11+
public function authorize(): bool
12+
{
13+
return true;
14+
}
15+
16+
public function rules(): array
17+
{
18+
$contextId = Application::get()->getRequest()->getContext()->getId();
19+
20+
// Build allowed stage IDs (values)
21+
$stages = Application::getApplicationStages();
22+
$stageIds = array_values(array_unique(array_filter(
23+
array_map('intval', array_merge(array_keys((array)$stages), array_values((array)$stages))),
24+
fn ($id) => $id > 0
25+
)));
26+
27+
// Context-scoped email keys
28+
$emailKeys = \APP\facades\Repo::emailTemplate()
29+
->getCollector($contextId)
30+
->getMany()
31+
->map(fn ($t) => $t->getData('key'))
32+
->filter()
33+
->values()
34+
->all();
35+
36+
return [
37+
// all fields are optional but if present must validate
38+
'stageId' => ['sometimes', 'integer', Rule::in($stageIds)],
39+
'title' => ['sometimes', 'string', 'max:255'],
40+
'include' => ['sometimes', 'boolean'],
41+
'emailTemplateKey' => ['sometimes', 'nullable', 'string', 'max:255', Rule::in($emailKeys)],
42+
'type' => ['sometimes','integer','in:1,2'],
43+
44+
'userGroupIds' => ['sometimes', 'array', 'min:1'],
45+
'userGroupIds.*' => [
46+
'integer',
47+
'distinct',
48+
Rule::exists('user_groups', 'user_group_id')
49+
->where(fn ($q) => $q->where('context_id', $contextId)),
50+
],
51+
];
52+
}
53+
54+
protected function prepareForValidation(): void
55+
{
56+
$stageId = $this->input('stageId', null);
57+
$type = $this->input('type', null);
58+
59+
$this->merge([
60+
'include' => $this->has('include')
61+
? filter_var($this->input('include'), FILTER_VALIDATE_BOOLEAN)
62+
: $this->input('include', null),
63+
64+
'userGroupIds' => $this->has('userGroupIds')
65+
? array_values(array_map('intval', (array) $this->input('userGroupIds', [])))
66+
: $this->input('userGroupIds', null),
67+
68+
'stageId' => $this->has('stageId')
69+
? (is_null($stageId) ? null : (int) $stageId)
70+
: $this->input('stageId', null),
71+
72+
'type' => $this->has('type')
73+
? (is_null($type) ? null : (int) $type)
74+
: $this->input('type', null),
75+
]);
76+
}
77+
78+
protected function passedValidation(): void
79+
{
80+
if ($this->has('emailTemplateKey')) {
81+
$key = $this->input('emailTemplateKey');
82+
$this->merge(['emailTemplateKey' => is_string($key) ? trim($key) : $key]);
83+
}
84+
}
85+
}

api/v1/editTaskTemplates/resources/TaskTemplateResource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function toArray(Request $request)
2929
'title' => $this->title,
3030
'include' => (bool) $this->include,
3131
'emailTemplateKey' => $this->email_template_key ?? null,
32+
'type' => (int) $this->type,
3233
'userGroups' => $this->whenLoaded(
3334
'userGroups',
3435
fn () => $this->userGroups

classes/editorialTask/Template.php

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Illuminate\Database\Eloquent\Builder;
2222
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
2323
use Illuminate\Database\Eloquent\Relations\BelongsTo;
24+
use Illuminate\Support\Facades\DB;
2425
use PKP\core\traits\ModelWithSettings;
2526
use PKP\userGroup\UserGroup;
2627

@@ -33,13 +34,17 @@ class Template extends Model
3334

3435
public $timestamps = true;
3536

37+
public const TYPE_DISCUSSION = 1;
38+
public const TYPE_TASK = 2;
39+
3640
// columns on edit_task_templates
3741
protected $fillable = [
3842
'stage_id',
3943
'title',
4044
'context_id',
4145
'include',
4246
'email_template_key',
47+
'type',
4348
];
4449

4550
protected $casts = [
@@ -48,6 +53,7 @@ class Template extends Model
4853
'include' => 'bool',
4954
'title' => 'string',
5055
'email_template_key' => 'string',
56+
'type' => 'int',
5157
];
5258

5359
/**
@@ -100,11 +106,11 @@ public function emailTemplate(): BelongsTo
100106
return $this->belongsTo(
101107
\PKP\emailTemplate\EmailTemplate::class,
102108
'email_template_key', // FK on this model
103-
'email_key' // owner key on email_templates
109+
'email_key' // owner key on email_templates
104110
)
105111
->where(function ($q) {
106112
$q->where('context_id', $this->context_id)
107-
->orWhereNull('context_id');
113+
->orWhereNull('context_id');
108114
})
109115
->orderByRaw('CASE WHEN context_id = ? THEN 1 ELSE 0 END DESC', [$this->context_id]);
110116
}
@@ -138,9 +144,9 @@ public function scopeOrderByPkDesc(Builder $query): Builder
138144
return $query->orderByDesc($query->getModel()->getKeyName());
139145
}
140146

141-
/**
142-
* Scope: filter by stage_id
143-
*/
147+
/**
148+
* Scope: filter by stage_id
149+
*/
144150
public function scopeFilterByStageId(Builder $query, int $stageId): Builder
145151
{
146152
return $query->where('stage_id', $stageId);
@@ -162,4 +168,69 @@ public function scopeFilterByEmailTemplateKey(Builder $query, string $key): Buil
162168
return $query->where('email_template_key', $key);
163169
}
164170

165-
}
171+
/**
172+
* Scope: filter by type
173+
*/
174+
public function scopeFilterByType(Builder $q, int $type): Builder
175+
{
176+
return $q->where('type', $type);
177+
}
178+
179+
/**
180+
* Scope: filter by title LIKE
181+
*/
182+
public function scopeFilterByTitleLike(Builder $query, string $title): Builder
183+
{
184+
$title = trim($title);
185+
if ($title === '') {
186+
return $query;
187+
}
188+
$needle = '%' . str_replace(['%', '_'], ['\\%', '\\_'], mb_strtolower($title)) . '%';
189+
return $query->whereRaw('LOWER(title) LIKE ?', [$needle]);
190+
}
191+
192+
/**
193+
* free-text/ words search across:
194+
* title column
195+
* name, description
196+
* email_template_key column
197+
*
198+
*/
199+
public function scopeFilterBySearch(Builder $query, string $phrase): Builder
200+
{
201+
$phrase = trim($phrase);
202+
if ($phrase === '') {
203+
return $query;
204+
}
205+
206+
$tokens = preg_split('/\s+/', $phrase) ?: [];
207+
$tokens = array_values(array_filter($tokens, fn ($t) => $t !== ''));
208+
if (!$tokens) {
209+
return $query;
210+
}
211+
212+
$settingsTable = $this->getSettingsTable(); // 'edit_task_template_settings'
213+
$pk = $this->getKeyName(); // 'edit_task_template_id'
214+
$selfTable = $this->getTable(); // 'edit_task_templates'
215+
216+
return $query->where(function (Builder $outer) use ($tokens, $settingsTable, $pk, $selfTable) {
217+
foreach ($tokens as $tok) {
218+
// escape % and _
219+
$like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], mb_strtolower($tok)) . '%';
220+
221+
$outer->where(function (Builder $q) use ($like, $settingsTable, $pk, $selfTable) {
222+
$q->whereRaw('LOWER(title) LIKE ?', [$like])
223+
->orWhereRaw('LOWER(email_template_key) LIKE ?', [$like])
224+
->orWhereExists(function ($sub) use ($like, $settingsTable, $pk, $selfTable) {
225+
$sub->select(DB::raw(1))
226+
->from($settingsTable . ' as ets')
227+
->whereColumn("ets.$pk", "$selfTable.$pk")
228+
->whereIn('ets.setting_name', ['name', 'description'])
229+
->whereRaw('LOWER(ets.setting_value) LIKE ?', [$like]);
230+
});
231+
});
232+
}
233+
});
234+
}
235+
236+
}

classes/migration/install/SubmissionsMigration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,11 @@ public function up(): void
315315
$table->index(['context_id'], 'edit_task_templates_context_id_idx');
316316

317317
$table->boolean('include')->default(false);
318+
$table->unsignedSmallInteger('type')->default(1); // 1 - discussion, 2 - task
318319
// store key, comes from either email_templates or email_templates_default_data
319320
$table->string('email_template_key', 255)->nullable();
320321
$table->index(['context_id', 'email_template_key'], 'edit_task_templates_context_email_key_idx');
322+
$table->index(['context_id', 'type', 'stage_id'], 'ett_context_type_stage_idx');
321323

322324

323325
$table->timestamps();

0 commit comments

Comments
 (0)