Skip to content

Commit 779c9b8

Browse files
committed
#11885 Implemented edit, delete, and search endpoints for task templates and fixed stageId on add
1 parent 93a5546 commit 779c9b8

File tree

6 files changed

+295
-1
lines changed

6 files changed

+295
-1
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\CanAccessSettingsPolicy;
3030
use PKP\security\authorization\ContextAccessPolicy;
3131
use PKP\security\Role;
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

@@ -93,6 +96,7 @@ public function add(AddTaskTemplate $illuminateRequest): JsonResponse
9396
'include' => $validated['include'] ?? false,
9497
'description' => $validated['description'] ?? null,
9598
'dueInterval' => $validated['dueInterval'] ?? null,
99+
'type' => (int) $validated['type'],
96100
]);
97101

98102
$tpl->userGroups()->sync($validated['userGroupIds']);
@@ -120,6 +124,9 @@ public function getMany(Request $request): JsonResponse
120124
->with('userGroups');
121125

122126
$queryParams = $this->_processAllowedParams($request->query(), [
127+
'search',
128+
'title',
129+
'type',
123130
'stageId',
124131
'include',
125132
'count',
@@ -128,6 +135,20 @@ public function getMany(Request $request): JsonResponse
128135

129136
foreach ($queryParams as $param => $val) {
130137
switch ($param) {
138+
case 'search':
139+
$collector->filterBySearch((string) $val);
140+
break;
141+
case 'title':
142+
$collector->filterByTitleLike((string) $val);
143+
break;
144+
case 'type':
145+
if (is_numeric($val)) {
146+
$type = (int) $val;
147+
if ($type === Template::TYPE_DISCUSSION || $type === Template::TYPE_TASK) {
148+
$collector->filterByType($type);
149+
}
150+
}
151+
break;
131152
case 'stageId':
132153
$collector->filterByStageId((int) $val);
133154
break;
@@ -156,4 +177,66 @@ public function getMany(Request $request): JsonResponse
156177
->setStatusCode(Response::HTTP_OK);
157178
}
158179

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

api/v1/editTaskTemplates/formRequests/AddTaskTemplate.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,22 @@ public function authorize(): bool
3131
public function rules(): array
3232
{
3333
$contextId = Application::get()->getRequest()->getContext()->getId();
34-
$stageIds = array_keys(Application::getApplicationStages());
34+
35+
// build a list of allowed stage IDs from values
36+
$stages = Application::getApplicationStages();
37+
$stageIds = array_values(array_unique(array_filter(
38+
array_map('intval', array_merge(array_keys((array)$stages), array_values((array)$stages))),
39+
fn ($id) => $id > 0
40+
)));
41+
3542
return [
3643
'type' => ['required', Rule::in(array_column(EditorialTaskType::cases(), 'value'))],
3744
'stageId' => ['required', 'integer', Rule::in($stageIds)],
3845
'title' => ['required', 'string', 'max:255'],
3946
'include' => ['boolean'],
4047
'dueInterval' => ['sometimes', 'nullable', 'string', Rule::in(array_column(EditorialTaskDueInterval::cases(), 'value'))],
4148
'description' => ['sometimes', 'nullable', 'string'],
49+
'type' => ['required','integer','in:1,2'],
4250
'userGroupIds' => ['required', 'array', 'min:1'],
4351
'userGroupIds.*' => [
4452
'integer',
@@ -52,10 +60,12 @@ public function rules(): array
5260
protected function prepareForValidation(): void
5361
{
5462
$stageId = $this->input('stageId', null);
63+
$type = $this->input('type', null);
5564
$this->merge([
5665
'include' => filter_var($this->input('include', false), FILTER_VALIDATE_BOOLEAN),
5766
'userGroupIds' => array_values(array_map('intval', (array) $this->input('userGroupIds', []))),
5867
'stageId' => is_null($stageId) ? $stageId : (int) $stageId,
68+
'type' => is_null($type) ? null : (int) $type,
5969
]);
6070
}
6171
}
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
@@ -31,6 +31,7 @@ public function toArray(Request $request)
3131
'include' => (bool) $this->include,
3232
'dueInterval' => $this->dueInterval,
3333
'description' => $this->description,
34+
'type' => (int) $this->type,
3435
'userGroups' => $this->whenLoaded(
3536
'userGroups',
3637
fn () => $this->userGroups

classes/editorialTask/Template.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use PKP\editorialTask\EditorialTask as Task;
2828
use PKP\stageAssignment\StageAssignment;
2929
use PKP\userGroup\UserGroup;
30+
use Illuminate\Support\Facades\DB;
3031

3132
class Template extends Model
3233
{
@@ -37,6 +38,9 @@ class Template extends Model
3738

3839
public $timestamps = true;
3940

41+
public const TYPE_DISCUSSION = 1;
42+
public const TYPE_TASK = 2;
43+
4044
// columns on edit_task_templates
4145
protected $fillable = [
4246
'stageId',
@@ -53,6 +57,7 @@ class Template extends Model
5357
'context_id' => 'int',
5458
'include' => 'bool',
5559
'title' => 'string',
60+
'type' => 'int',
5661
];
5762

5863
/**
@@ -172,4 +177,69 @@ public function promote(Submission $submission): Task
172177
]);
173178
}
174179

180+
/**
181+
* Scope: filter by type
182+
*/
183+
public function scopeFilterByType(Builder $q, int $type): Builder
184+
{
185+
return $q->where('type', $type);
186+
}
187+
188+
/**
189+
* Scope: filter by title LIKE
190+
*/
191+
public function scopeFilterByTitleLike(Builder $query, string $title): Builder
192+
{
193+
$title = trim($title);
194+
if ($title === '') {
195+
return $query;
196+
}
197+
$needle = '%' . str_replace(['%', '_'], ['\\%', '\\_'], mb_strtolower($title)) . '%';
198+
return $query->whereRaw('LOWER(title) LIKE ?', [$needle]);
199+
}
200+
201+
/**
202+
* free-text/ words search across:
203+
* title column
204+
* name, description
205+
* email_template_key column
206+
*
207+
*/
208+
public function scopeFilterBySearch(Builder $query, string $phrase): Builder
209+
{
210+
$phrase = trim($phrase);
211+
if ($phrase === '') {
212+
return $query;
213+
}
214+
215+
$tokens = preg_split('/\s+/', $phrase) ?: [];
216+
$tokens = array_values(array_filter($tokens, fn ($t) => $t !== ''));
217+
if (!$tokens) {
218+
return $query;
219+
}
220+
221+
$settingsTable = $this->getSettingsTable(); // 'edit_task_template_settings'
222+
$pk = $this->getKeyName(); // 'edit_task_template_id'
223+
$selfTable = $this->getTable(); // 'edit_task_templates'
224+
225+
return $query->where(function (Builder $outer) use ($tokens, $settingsTable, $pk, $selfTable) {
226+
foreach ($tokens as $tok) {
227+
// escape % and _
228+
$like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], mb_strtolower($tok)) . '%';
229+
230+
$outer->where(function (Builder $q) use ($like, $settingsTable, $pk, $selfTable) {
231+
$q->whereRaw('LOWER(title) LIKE ?', [$like])
232+
->orWhereRaw('LOWER(email_template_key) LIKE ?', [$like])
233+
->orWhereExists(function ($sub) use ($like, $settingsTable, $pk, $selfTable) {
234+
$sub->select(DB::raw(1))
235+
->from($settingsTable . ' as ets')
236+
->whereColumn("ets.$pk", "$selfTable.$pk")
237+
->whereIn('ets.setting_name', ['name', 'description'])
238+
->whereRaw('LOWER(ets.setting_value) LIKE ?', [$like]);
239+
});
240+
});
241+
}
242+
});
243+
}
244+
175245
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace PKP\migration\upgrade\v3_6_0;
4+
5+
use Illuminate\Database\Migrations\Migration;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
class I11885_AddTypeToEditTaskTemplates extends Migration
11+
{
12+
public function up(): void
13+
{
14+
// add column with default
15+
Schema::table('edit_task_templates', function (Blueprint $table) {
16+
if (!Schema::hasColumn('edit_task_templates', 'type')) {
17+
$table->unsignedSmallInteger('type')->default(1);
18+
}
19+
});
20+
21+
// backfill if any task exists with type=2 for a template, set that template to 2
22+
DB::table('edit_task_templates as ett')
23+
->whereExists(function ($q) {
24+
$q->select(DB::raw(1))
25+
->from('edit_tasks as et')
26+
->whereColumn('et.edit_task_template_id', 'ett.edit_task_template_id')
27+
->where('et.type', 2);
28+
})
29+
->update(['type' => 2]);
30+
31+
// index to speed filtering (context + type + stage)
32+
try {
33+
Schema::table('edit_task_templates', function (Blueprint $table) {
34+
$table->index(['context_id', 'type', 'stage_id'], 'ett_context_type_stage_idx');
35+
});
36+
} catch (\Throwable $e) {
37+
// index may already exist.. ignore
38+
}
39+
}
40+
41+
public function down(): void
42+
{
43+
throw new \PKP\install\DowngradeNotSupportedException();
44+
}
45+
}

0 commit comments

Comments
 (0)