Skip to content
Merged
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
106 changes: 44 additions & 62 deletions ProcessMaker/Traits/TaskControllerIndexMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\QueryException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use ProcessMaker\Filters\Filter;
use ProcessMaker\Managers\DataManager;
use ProcessMaker\Models\Process;
Expand Down Expand Up @@ -69,7 +70,7 @@ private function applyFilters($query, $request)
}

$filterByFields = [
'process_id',
'process_request_tokens.process_id' => 'process_id',
'process_request_tokens.user_id' => 'user_id',
'process_request_tokens.status' => 'status',
'element_id',
Expand Down Expand Up @@ -247,60 +248,6 @@ private function applyColumnOrdering($query, $request)
'is_priority',
];

$hasProcessRequestOrdering = false;
$hasUserOrdering = false;
foreach ($orderColumns as $column) {
$normalizedColumn = preg_replace(
'/^(process_request|processRequests)\./',
'process_requests.',
$column
);

if (in_array($normalizedColumn, array_map(
fn ($name) => 'process_requests.' . $name,
$processRequestColumns
), true) || preg_match('/^data\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/', $column)) {
$hasProcessRequestOrdering = true;
}
if ($column === 'user.name') {
$hasUserOrdering = true;
}
}

if ($hasProcessRequestOrdering) {
$query->leftJoin(
'process_requests',
'process_requests.id',
'=',
'process_request_tokens.process_request_id'
);
}
if ($hasUserOrdering) {
$query->leftJoin(
'users',
'users.id',
'=',
'process_request_tokens.user_id'
);
}
if ($hasProcessRequestOrdering || $hasUserOrdering) {
if ($query->getQuery()->columns === null) {
$query->select('process_request_tokens.*');
} else {
$query->select(array_map(function ($column) {
if (!is_string($column)) {
return $column;
}

$column = ltrim($column, '.');

return str_contains($column, '.')
? $column
: 'process_request_tokens.' . $column;
}, $query->getQuery()->columns));
}
}

$hasValidOrdering = false;
foreach ($orderColumns as $index => $column) {
$direction = strtolower($orderDirections[$index] ?? $orderDirections[0] ?? 'asc');
Expand All @@ -312,17 +259,44 @@ private function applyColumnOrdering($query, $request)
);

if ($column === 'user.name') {
$query->orderBy('users.firstname', $direction)
->orderBy('users.lastname', $direction);
$query->orderBy(
$this->relatedOrderSubquery('users', 'firstname', 'users.id', 'process_request_tokens.user_id'),
$direction
)->orderBy(
$this->relatedOrderSubquery('users', 'lastname', 'users.id', 'process_request_tokens.user_id'),
$direction
);
$hasValidOrdering = true;
} elseif (preg_match('/^data\.([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)$/', $column, $matches)) {
$query->orderBy('process_requests.data->' . str_replace('.', '->', $matches[1]), $direction);
$jsonColumn = 'data->' . str_replace('.', '->', $matches[1]);
$query->orderBy(
$this->relatedOrderSubquery(
'process_requests',
$jsonColumn,
'process_requests.id',
'process_request_tokens.process_request_id'
),
$direction
);
$hasValidOrdering = true;
} elseif (in_array($normalizedColumn, array_map(
fn ($name) => 'process_requests.' . $name,
$processRequestColumns
), true)) {
$query->orderBy($normalizedColumn, $direction);
$columnName = substr($normalizedColumn, strlen('process_requests.'));
if ($columnName === 'id') {
$query->orderBy('process_request_tokens.process_request_id', $direction);
} else {
$query->orderBy(
$this->relatedOrderSubquery(
'process_requests',
$columnName,
'process_requests.id',
'process_request_tokens.process_request_id'
),
$direction
);
}
$hasValidOrdering = true;
} elseif (in_array($column, $tokenColumns, true)) {
$query->orderBy('process_request_tokens.' . $column, $direction);
Expand All @@ -335,14 +309,22 @@ private function applyColumnOrdering($query, $request)
}
}

private function relatedOrderSubquery(string $table, string $column, string $localKey, string $foreignKey)
{
return DB::table($table)
->select($column)
->whereColumn($localKey, $foreignKey)
->limit(1);
}

private function applyStatusFilter($query, $request)
{
$statusFilter = $request->input('statusfilter', '');
if ($statusFilter) {
$statusFilter = array_map(function ($value) {
return mb_strtoupper(trim($value));
}, explode(',', $statusFilter));
$query->whereIn('status', $statusFilter);
$query->whereIn('process_request_tokens.status', $statusFilter);
}
}

Expand Down Expand Up @@ -544,8 +526,8 @@ private function applyForCurrentUser($query, $user)
}

$query->where(function ($query) use ($user) {
$query->where('user_id', $user->id)
->orWhereIn('id', $user->availableSelfServiceTasksQuery());
$query->where('process_request_tokens.user_id', $user->id)
->orWhereIn('process_request_tokens.id', $user->availableSelfServiceTasksQuery());
});
}

Expand Down
193 changes: 193 additions & 0 deletions tests/Feature/Api/TaskListNonAdminCaseTitleOrderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php

namespace Tests\Feature\Api;

use PHPUnit\Framework\Attributes\Group as TestGroup;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
use ProcessMaker\Models\User;
use Tests\Feature\Shared\RequestHelper;
use Tests\TestCase;

/**
* Regression for tasks index 1052 (ambiguous user_id) when a non-admin
* lists tasks ordered by process_requests.case_title.
*/
#[TestGroup('process_tests')]
class TaskListNonAdminCaseTitleOrderTest extends TestCase
{
use RequestHelper;

public function testNonAdminCanListTasksOrderedByCaseTitleDesc()
{
$this->user = User::factory()->create([
'is_administrator' => false,
'status' => 'ACTIVE',
]);
$otherUser = User::factory()->create([
'is_administrator' => false,
'status' => 'ACTIVE',
]);

$zetaRequest = ProcessRequest::factory()->create();
$alphaRequest = ProcessRequest::factory()->create();
$otherRequest = ProcessRequest::factory()->create();
$zetaRequest->forceFill(['case_title' => 'Zeta mandate'])->saveQuietly();
$alphaRequest->forceFill(['case_title' => 'Alpha mandate'])->saveQuietly();
$otherRequest->forceFill(['case_title' => 'Omega mandate'])->saveQuietly();

$zetaTask = ProcessRequestToken::factory()->create([
'user_id' => $this->user->id,
'process_id' => $zetaRequest->process_id,
'process_request_id' => $zetaRequest->id,
'element_type' => 'task',
'status' => 'ACTIVE',
'is_self_service' => false,
'is_priority' => true,
'completed_at' => null,
]);
$alphaTask = ProcessRequestToken::factory()->create([
'user_id' => $this->user->id,
'process_id' => $alphaRequest->process_id,
'process_request_id' => $alphaRequest->id,
'element_type' => 'task',
'status' => 'ACTIVE',
'is_self_service' => false,
'is_priority' => true,
'completed_at' => null,
]);
ProcessRequestToken::factory()->create([
'user_id' => $otherUser->id,
'process_id' => $otherRequest->process_id,
'process_request_id' => $otherRequest->id,
'element_type' => 'task',
'status' => 'ACTIVE',
'is_self_service' => false,
'is_priority' => true,
'completed_at' => null,
]);

$response = $this->apiCall('GET', '/tasks', [
'page' => 1,
'include' => 'process,processRequest,processRequest.user,user',
'pmql' => '(user_id = ' . $this->user->id . ')',
'per_page' => 15,
'order_by' => 'process_requests.case_title',
'order_direction' => 'desc',
'non_system' => true,
'processesIManage' => 'false',
'advanced_filter' => json_encode([
[
'subject' => ['type' => 'Status'],
'operator' => '=',
'value' => 'In Progress',
],
[
'subject' => ['type' => 'Field', 'value' => 'is_priority'],
'operator' => '=',
'value' => true,
],
]),
]);

$response->assertStatus(200);

$rows = $response->json('data');
$this->assertCount(2, $rows);
$this->assertSame(
[$zetaTask->id, $alphaTask->id],
array_column($rows, 'id')
);
$this->assertSame(
['Zeta mandate', 'Alpha mandate'],
array_map(fn ($row) => $row['process_request']['case_title'] ?? null, $rows)
);
}

public function testNonAdminCanListTasksOrderedByCaseTitleDescAndFulltextSearch()
{
$this->user = User::factory()->create([
'is_administrator' => false,
'status' => 'ACTIVE',
]);
$otherUser = User::factory()->create([
'is_administrator' => false,
'status' => 'ACTIVE',
]);

$zetaRequest = ProcessRequest::factory()->create();
$alphaRequest = ProcessRequest::factory()->create();
$otherRequest = ProcessRequest::factory()->create();
$zetaRequest->forceFill(['case_title' => 'Zeta mandate'])->saveQuietly();
$alphaRequest->forceFill(['case_title' => 'Alpha mandate'])->saveQuietly();
$otherRequest->forceFill(['case_title' => 'Omega mandate'])->saveQuietly();

$zetaTask = ProcessRequestToken::factory()->create([
'user_id' => $this->user->id,
'process_id' => $zetaRequest->process_id,
'process_request_id' => $zetaRequest->id,
'element_name' => 'Review of the client name',
'element_type' => 'task',
'status' => 'ACTIVE',
'is_self_service' => false,
'is_priority' => true,
'completed_at' => null,
]);
$alphaTask = ProcessRequestToken::factory()->create([
'user_id' => $this->user->id,
'process_id' => $alphaRequest->process_id,
'process_request_id' => $alphaRequest->id,
'element_type' => 'task',
'status' => 'ACTIVE',
'is_self_service' => false,
'is_priority' => true,
'completed_at' => null,
]);
ProcessRequestToken::factory()->create([
'user_id' => $otherUser->id,
'process_id' => $otherRequest->process_id,
'process_request_id' => $otherRequest->id,
'element_type' => 'task',
'status' => 'ACTIVE',
'is_self_service' => false,
'is_priority' => true,
'completed_at' => null,
]);

$response = $this->apiCall('GET', '/tasks', [
'page' => 1,
'include' => 'process,processRequest,processRequest.user,user',
'pmql' => '(user_id = ' . $this->user->id . ') AND (fulltext LIKE "%Review of the client name%") ',
'per_page' => 15,
'order_by' => 'process_requests.case_title',
'order_direction' => 'desc',
'non_system' => true,
'processesIManage' => 'false',
'advanced_filter' => json_encode([
[
'subject' => ['type' => 'Status'],
'operator' => '=',
'value' => 'In Progress',
],
[
'subject' => ['type' => 'Field', 'value' => 'is_priority'],
'operator' => '=',
'value' => true,
],
]),
]);

$response->assertStatus(200);

$rows = $response->json('data');
$this->assertCount(1, $rows);
$this->assertSame(
[$zetaTask->id],
array_column($rows, 'id')
);
$this->assertSame(
['Zeta mandate'],
array_map(fn ($row) => $row['process_request']['case_title'] ?? null, $rows)
);
}
}
Loading