From 89ab186b9a4eaebb9030c9ba89c423a744a2e2a7 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 11 Jul 2023 16:11:53 -0700 Subject: [PATCH 1/8] Add rollback class --- ProcessMaker/RollbackProcessRequest.php | 157 ++++++++++++++++++ .../RollbackProcessRequestTest.php | 156 +++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 ProcessMaker/RollbackProcessRequest.php create mode 100644 tests/unit/ProcessMaker/RollbackProcessRequestTest.php diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php new file mode 100644 index 0000000000..a99964a2d0 --- /dev/null +++ b/ProcessMaker/RollbackProcessRequest.php @@ -0,0 +1,157 @@ +currentTask->processRequest; + + return $processRequest->tokens() + ->where('status', 'CLOSED') + ->where('id', '<', $this->currentTask->id) + ->where('element_id', '!=', $this->currentTask->element_id) + ->whereIn('element_type', $this->eligibleTypes) + ->orderBy('id', 'desc') + ->first(); + } + + /** + * Rollback a token to the previous token and reset the request status. + * + * @param ProcessRequestToken $task + * @return ProcessRequestToken + */ + public function rollback( + ProcessRequestToken $currentTask, + BpmnDocument $processDefinitions + ) : ProcessRequestToken { + $this->currentTask = $currentTask; + $this->processDefinitions = $processDefinitions; + $this->rollbackToTask = $rollbackToTask = $this->eligibleRollbackTask(); + if (!$this->rollbackToTask) { + throw new \Exception('No eligible rollback task found'); + } + + switch ($this->rollbackToTask->element_type) { + case 'scriptTask': + $this->rollbackToScriptTask(); + break; + case 'serviceTask': + $this->rollbackToServiceTask(); + break; + case 'task': + $this->rollbackToTask(); + break; + } + + $processRequest = $this->newTask->processRequest; + $processRequest->status = 'ACTIVE'; + $processRequest->saveOrFail(); + + return $this->newTask; + } + + private function rollbackToTask() + { + $this->copyTask(); + + if ($this->newTask->user_id) { + $this->newTask->sendActivityActivatedNotifications(); + } + event(new ActivityAssigned($this->newTask)); + + $this->addComment(); + } + + private function rollbackToScriptTask() + { + $this->copyTask(); + + $bpmnTask = $this->processDefinitions->getEvent($this->newTask->element_id); + + WorkflowManager::runScripTask($bpmnTask, $this->newTask); + } + + private function copyTask() + { + $newTask = $this->rollbackToTask->replicate(); + $newTask->uuid = null; + $newTask->status = 'ACTIVE'; + $newTask->saveOrFail(); + $this->newTask = $newTask; + } + + /** + * Add a comment that the task was rolled back. + * + * @return void + */ + private function addComment() : void + { + $user = Auth::user(); + $userName = $user ? $user->name : __('The System'); + Comment::create([ + 'type' => 'LOG', + 'user_id' => $user ? $user->id : null, + 'commentable_type' => ProcessRequest::class, + 'commentable_id' => $this->currentTask->process_request_id, + 'subject' => 'Rollback', + 'body' => __(':user rolled back :failed_task_name to :new_task_name', [ + 'user' => $userName, + 'failed_task_name' => $this->currentTask->element_name, + 'new_task_name' => $this->newTask->element_name, + ]), + ]); + } +} diff --git a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php new file mode 100644 index 0000000000..374c53425e --- /dev/null +++ b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php @@ -0,0 +1,156 @@ +user = User::factory()->create(); + Auth::login($this->user); + + $bpmn = file_get_contents(__DIR__ . '/../../Fixtures/rollback_test.bpmn'); + + $workingScript = Script::factory()->create(['code' => ' 1]; ']); + $workingServiceTask = Script::factory()->create([ + 'key' => 'test/foo', + 'code' => ' 1]; ', + ]); + $errorScript = Script::factory()->create(['code' => 'id, $bpmn); + $bpmn = str_replace('[error_script_id]', $errorScript->id, $bpmn); + + $this->process = Process::factory()->create([ + 'bpmn' => $bpmn, + ]); + $this->processRequest = ProcessRequest::factory()->create([ + 'process_id' => $this->process->id, + ]); + ProcessTaskAssignment::factory()->create([ + 'process_id' => $this->process->id, + 'process_task_id' => 'node_45', + 'assignment_id' => $this->user->id, + 'assignment_type' => 'user', + ]); + } + + // public function testRollbackToFormTask() + // { + // $definitions = $this->process->getDefinitions(); + // $startEvent = $definitions->getEvent('node_1'); + // $request = WorkflowManager::triggerStartEvent($this->process, $startEvent, []); + // $workingFormTask = $request->tokens()->where('element_id', 'node_45')->first(); + // WorkflowManager::completeTask($this->process, $request, $workingFormTask, []); + + // $failedTask = $request->tokens()->where('element_id', 'node_145')->first(); + // $newTask = RollbackProcessRequest::rollback($failedTask); + + // $this->assertEquals('node_45', $newTask->element_id); + // $this->assertEquals('ACTIVE', $newTask->status); + // } + + // public function testRollbackToScriptTask() + // { + // $definitions = $this->process->getDefinitions(); + // $startEvent = $definitions->getEvent('node_1'); + // $request = WorkflowManager::triggerStartEvent($this->process, $startEvent, []); + + // $failedTask = $request->tokens()->where('element_id', 'node_145')->orderBy('id', 'desc')->first(); + // $newTask = RollbackProcessRequest::rollback($failedTask); + + // $this->assertEquals('node_2', $newTask->element_id); + // $this->assertEquals('ACTIVE', $newTask->status); + // } + + public function testRollbackToFormTask() + { + $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); + $task1 = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_5', + 'element_type' => 'task', + ]); + $task2 = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_6', + 'element_type' => 'gateway', + ]); + $task3 = ProcessRequestToken::factory()->create([ + 'status' => 'FAILING', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_7', + 'element_type' => 'scriptTask', + ]); + + $newTask = RollbackProcessRequest::rollback($task3); + $this->assertEquals('node_5', $newTask->element_id); + $this->assertEquals('ACTIVE', $newTask->status); + + $comment = Comment::orderBy('id', 'desc')->first(); + $this->assertEquals($comment->body, "The System rolled back {$task3->element_name} to {$newTask->element_name}"); + } + + public function testRollbackToScriptTask() + { + $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); + $task1 = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_5', + 'element_type' => 'scriptTask', + ]); + $task2 = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_6', + 'element_type' => 'gateway', + ]); + $task3 = ProcessRequestToken::factory()->create([ + 'status' => 'FAILING', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_7', + 'element_type' => 'scriptTask', + ]); + + $mocksScriptTask = Mockery::mock(ScriptTaskInterface::class); + $mockProcessDefinitions = Mockery::mock(BpmnDocument::class); + $mockProcessDefinitions->shouldReceive('getEvent') + ->with('node_5') + ->andReturn($mocksScriptTask); + WorkflowManager::shouldReceive('runScripTask') + ->withArgs(function ($scriptTask, $task) use ($mocksScriptTask) { + return $scriptTask === $mocksScriptTask && $task->element_id = 'node_5'; + }); + + $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); + $this->assertEquals('node_5', $newTask->element_id); + $this->assertEquals('ACTIVE', $newTask->status); + } +} From feabacf4f13b5d405d0f704f2e3f9b5c1909cbd4 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Tue, 11 Jul 2023 18:20:19 -0700 Subject: [PATCH 2/8] Add frontend components for rollback --- .../Http/Controllers/Api/TaskController.php | 19 ++++ .../Http/Controllers/RequestController.php | 10 ++- ProcessMaker/RollbackProcessRequest.php | 32 +++++-- resources/js/components/Timeline.vue | 1 + resources/views/requests/show.blade.php | 20 +++++ routes/api.php | 2 + .../RollbackProcessRequestTest.php | 87 +++---------------- 7 files changed, 91 insertions(+), 80 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index 2d556c62f3..ab1537c006 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Http\Controllers\Api; use Carbon\Carbon; +use Facades\ProcessMaker\RollbackProcessRequest; use Illuminate\Database\QueryException; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -417,4 +418,22 @@ public function getScreen(Request $request, ProcessRequestToken $task, Screen $s // Authorized in policy return new ApiResource($screen->versionFor($task->processRequest)); } + + public function eligibleRollbackTask(Request $request, ProcessRequestToken $task) + { + $eligibleTask = RollbackProcessRequest::eligibleRollbackTask($task); + if (!$eligibleTask) { + return ['message' => __('Task can not be rolled back')]; + } + + return new Resource($eligibleTask); + } + + public function rollbackTask(Request $request, ProcessRequestToken $task) + { + $processDefinitions = $task->processRequest->getVersionDefinitions(); + $newTask = RollbackProcessRequest::rollback($task, $processDefinitions); + + return new Resource($newTask); + } } diff --git a/ProcessMaker/Http/Controllers/RequestController.php b/ProcessMaker/Http/Controllers/RequestController.php index 5bdbc2c01e..910206cde2 100644 --- a/ProcessMaker/Http/Controllers/RequestController.php +++ b/ProcessMaker/Http/Controllers/RequestController.php @@ -2,6 +2,7 @@ namespace ProcessMaker\Http\Controllers; +use Facades\ProcessMaker\RollbackProcessRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; @@ -134,6 +135,12 @@ public function show(ProcessRequest $request, Media $mediaItems) $isProcessManager = $request->process?->manager_id === Auth::user()->id; + $eligibleRollbackTask = null; + $errorTask = RollbackProcessRequest::getErrorTask($request); + if ($errorTask) { + $eligibleRollbackTask = RollbackProcessRequest::eligibleRollbackTask($errorTask); + } + return view('requests.show', compact( 'request', 'files', @@ -145,7 +152,8 @@ public function show(ProcessRequest $request, Media $mediaItems) 'canPrintScreens', 'screenRequested', 'addons', - 'isProcessManager' + 'isProcessManager', + 'eligibleRollbackTask' )); } diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php index a99964a2d0..9b86d7c94b 100644 --- a/ProcessMaker/RollbackProcessRequest.php +++ b/ProcessMaker/RollbackProcessRequest.php @@ -47,20 +47,38 @@ class RollbackProcessRequest */ private $processDefinitions; + /** + * Return the last task if its status was failing. + * We may need to update this logic later. + * + * @param ProcessRequest $processRequest + * + * @return ProcessRequestToken|null + */ + public function getErrorTask(ProcessRequest $processRequest) : ?ProcessRequestToken + { + $lastTask = $processRequest->tokens()->orderBy('id', 'desc')->first(); + if ($lastTask->status === 'FAILING') { + return $lastTask; + } + + return null; + } + /** * Find an element in the request that can be rolled-back to. * Return null if none are eligible. * * @return ProcessRequestToken|null */ - public function eligibleRollbackTask() : ?ProcessRequestToken + public function eligibleRollbackTask(ProcessRequestToken $currentTask) : ?ProcessRequestToken { - $processRequest = $this->currentTask->processRequest; + $processRequest = $currentTask->processRequest; return $processRequest->tokens() ->where('status', 'CLOSED') - ->where('id', '<', $this->currentTask->id) - ->where('element_id', '!=', $this->currentTask->element_id) + ->where('id', '<', $currentTask->id) + ->where('element_id', '!=', $currentTask->element_id) ->whereIn('element_type', $this->eligibleTypes) ->orderBy('id', 'desc') ->first(); @@ -78,7 +96,7 @@ public function rollback( ) : ProcessRequestToken { $this->currentTask = $currentTask; $this->processDefinitions = $processDefinitions; - $this->rollbackToTask = $rollbackToTask = $this->eligibleRollbackTask(); + $this->rollbackToTask = $this->eligibleRollbackTask($currentTask); if (!$this->rollbackToTask) { throw new \Exception('No eligible rollback task found'); } @@ -121,6 +139,8 @@ private function rollbackToScriptTask() $bpmnTask = $this->processDefinitions->getEvent($this->newTask->element_id); WorkflowManager::runScripTask($bpmnTask, $this->newTask); + + $this->addComment(); } private function copyTask() @@ -140,7 +160,7 @@ private function copyTask() private function addComment() : void { $user = Auth::user(); - $userName = $user ? $user->name : __('The System'); + $userName = $user ? $user->fullname : __('The System'); Comment::create([ 'type' => 'LOG', 'user_id' => $user ? $user->id : null, diff --git a/resources/js/components/Timeline.vue b/resources/js/components/Timeline.vue index b92ff7d09b..2451ac3595 100644 --- a/resources/js/components/Timeline.vue +++ b/resources/js/components/Timeline.vue @@ -30,6 +30,7 @@ const SubjectIcons = { 'Task Complete': 'far fa-square', 'Gateway': 'far fa-square fa-rotate-45', + 'Rollback': 'fa fa-undo', }; export default { props: ["commentable_id", diff --git a/resources/views/requests/show.blade.php b/resources/views/requests/show.blade.php index 196a4456cf..0d9ef4857e 100644 --- a/resources/views/requests/show.blade.php +++ b/resources/views/requests/show.blade.php @@ -313,6 +313,18 @@ class="d-inline-flex pull-left align-items-center" :input-data="requestBy" displ @endif + @if ($eligibleRollbackTask) + @can('update', $request) +
  • +
    {{ __('Rollback Request') }}
    + + {{ __('Rollback to task') }}: {{ $eligibleRollbackTask->element_name }} ({{ $eligibleRollbackTask->element_id }}) +
  • + @endcan + @endif @if ($request->parentRequest)
  • {{ __('Parent Request') }}
    @@ -701,6 +713,14 @@ classStatusCard() { apiRequest ); }, + rollback(name) { + ProcessMaker.confirmModal( + this.$t('Confirm'), + this.$t('Are you sure you want to rollback to the task: ') + name, + 'default', + () => { console.log("foo") } + ) + }, getConfigurationComments() { if (this.canViewComments) { const commentsPackage = 'comment-editor' in Vue.options.components; diff --git a/routes/api.php b/routes/api.php index 40bda4153a..e942372a69 100644 --- a/routes/api.php +++ b/routes/api.php @@ -149,6 +149,8 @@ Route::get('tasks', [TaskController::class, 'index'])->name('tasks.index'); // Already filtered in controller Route::get('tasks/{task}', [TaskController::class, 'show'])->name('tasks.show')->middleware('can:view,task'); Route::get('tasks/{task}/screens/{screen}', [TaskController::class, 'getScreen'])->name('tasks.get_screen')->middleware('can:viewScreen,task,screen'); + Route::get('tasks/{task}/eligibleRollbackTask', [TaskController::class, 'eligibleRollbackTask'])->name('tasks.eligible_rollback_task')->middleware('can:update,task'); + Route::post('tasks/{task}/rollback', [TaskController::class, 'rollbackTask'])->name('tasks.rollback_task')->middleware('can:update,task'); // Requests Route::get('requests', [ProcessRequestController::class, 'index'])->name('requests.index'); // Already filtered in controller diff --git a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php index 374c53425e..b240c65643 100644 --- a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php +++ b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php @@ -6,7 +6,6 @@ use Illuminate\Support\Facades\Auth; use Mockery; use ProcessMaker\Facades\WorkflowManager; -use ProcessMaker\ImportExport\Psudomodels\Psudomodel; use ProcessMaker\Models\Comment; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; @@ -14,79 +13,11 @@ use ProcessMaker\Models\ProcessTaskAssignment; use ProcessMaker\Models\Script; use ProcessMaker\Models\User; -use ProcessMaker\Nayra\Contracts\Bpmn\EventInterface; use ProcessMaker\Nayra\Contracts\Bpmn\ScriptTaskInterface; -use ProcessMaker\Nayra\Contracts\Storage\BpmnDocumentInterface; use ProcessMaker\Repositories\BpmnDocument; class RollbackProcessRequestTest extends TestCase { - private $process; - - private $processRequest; - - private $user; - - public function setUpProcessRequest() - { - return; - - $this->user = User::factory()->create(); - Auth::login($this->user); - - $bpmn = file_get_contents(__DIR__ . '/../../Fixtures/rollback_test.bpmn'); - - $workingScript = Script::factory()->create(['code' => ' 1]; ']); - $workingServiceTask = Script::factory()->create([ - 'key' => 'test/foo', - 'code' => ' 1]; ', - ]); - $errorScript = Script::factory()->create(['code' => 'id, $bpmn); - $bpmn = str_replace('[error_script_id]', $errorScript->id, $bpmn); - - $this->process = Process::factory()->create([ - 'bpmn' => $bpmn, - ]); - $this->processRequest = ProcessRequest::factory()->create([ - 'process_id' => $this->process->id, - ]); - ProcessTaskAssignment::factory()->create([ - 'process_id' => $this->process->id, - 'process_task_id' => 'node_45', - 'assignment_id' => $this->user->id, - 'assignment_type' => 'user', - ]); - } - - // public function testRollbackToFormTask() - // { - // $definitions = $this->process->getDefinitions(); - // $startEvent = $definitions->getEvent('node_1'); - // $request = WorkflowManager::triggerStartEvent($this->process, $startEvent, []); - // $workingFormTask = $request->tokens()->where('element_id', 'node_45')->first(); - // WorkflowManager::completeTask($this->process, $request, $workingFormTask, []); - - // $failedTask = $request->tokens()->where('element_id', 'node_145')->first(); - // $newTask = RollbackProcessRequest::rollback($failedTask); - - // $this->assertEquals('node_45', $newTask->element_id); - // $this->assertEquals('ACTIVE', $newTask->status); - // } - - // public function testRollbackToScriptTask() - // { - // $definitions = $this->process->getDefinitions(); - // $startEvent = $definitions->getEvent('node_1'); - // $request = WorkflowManager::triggerStartEvent($this->process, $startEvent, []); - - // $failedTask = $request->tokens()->where('element_id', 'node_145')->orderBy('id', 'desc')->first(); - // $newTask = RollbackProcessRequest::rollback($failedTask); - - // $this->assertEquals('node_2', $newTask->element_id); - // $this->assertEquals('ACTIVE', $newTask->status); - // } - public function testRollbackToFormTask() { $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); @@ -109,7 +40,9 @@ public function testRollbackToFormTask() 'element_type' => 'scriptTask', ]); - $newTask = RollbackProcessRequest::rollback($task3); + $mockProcessDefinitions = $this->mockWorkflowManager(); + + $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); $this->assertEquals('node_5', $newTask->element_id); $this->assertEquals('ACTIVE', $newTask->status); @@ -139,18 +72,26 @@ public function testRollbackToScriptTask() 'element_type' => 'scriptTask', ]); + $mockProcessDefinitions = $this->mockWorkflowManager(); + + $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); + $this->assertEquals('node_5', $newTask->element_id); + $this->assertEquals('ACTIVE', $newTask->status); + } + + private function mockWorkflowManager() + { $mocksScriptTask = Mockery::mock(ScriptTaskInterface::class); $mockProcessDefinitions = Mockery::mock(BpmnDocument::class); $mockProcessDefinitions->shouldReceive('getEvent') ->with('node_5') ->andReturn($mocksScriptTask); WorkflowManager::shouldReceive('runScripTask') + ->zeroOrMoreTimes() ->withArgs(function ($scriptTask, $task) use ($mocksScriptTask) { return $scriptTask === $mocksScriptTask && $task->element_id = 'node_5'; }); - $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); - $this->assertEquals('node_5', $newTask->element_id); - $this->assertEquals('ACTIVE', $newTask->status); + return $mockProcessDefinitions; } } From e42b92d819f594200e14a7b710a724f090a243a2 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 12 Jul 2023 18:09:27 -0700 Subject: [PATCH 3/8] Add ability to rollback to service task --- .../Http/Controllers/Api/TaskController.php | 2 +- .../Http/Controllers/RequestController.php | 3 +- ProcessMaker/RollbackProcessRequest.php | 15 ++++++ resources/views/requests/show.blade.php | 12 +++-- tests/Feature/Api/TasksTest.php | 48 +++++++++++++++++ tests/Fixtures/rollback_test.bpmn | 50 +++++++++++++++++ .../RollbackProcessRequestTest.php | 54 +++++++++++++++++-- 7 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 tests/Fixtures/rollback_test.bpmn diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index ab1537c006..668d28cc44 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -431,7 +431,7 @@ public function eligibleRollbackTask(Request $request, ProcessRequestToken $task public function rollbackTask(Request $request, ProcessRequestToken $task) { - $processDefinitions = $task->processRequest->getVersionDefinitions(); + $processDefinitions = $task->process->getDefinitions(); $newTask = RollbackProcessRequest::rollback($task, $processDefinitions); return new Resource($newTask); diff --git a/ProcessMaker/Http/Controllers/RequestController.php b/ProcessMaker/Http/Controllers/RequestController.php index 910206cde2..1a0f7277e8 100644 --- a/ProcessMaker/Http/Controllers/RequestController.php +++ b/ProcessMaker/Http/Controllers/RequestController.php @@ -153,7 +153,8 @@ public function show(ProcessRequest $request, Media $mediaItems) 'screenRequested', 'addons', 'isProcessManager', - 'eligibleRollbackTask' + 'eligibleRollbackTask', + 'errorTask', )); } diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php index 9b86d7c94b..ae782a22f4 100644 --- a/ProcessMaker/RollbackProcessRequest.php +++ b/ProcessMaker/RollbackProcessRequest.php @@ -115,8 +115,12 @@ public function rollback( $processRequest = $this->newTask->processRequest; $processRequest->status = 'ACTIVE'; + $processRequest->process_version_id = $processRequest->process->getLatestVersion()->id; $processRequest->saveOrFail(); + $currentTask->status = 'CLOSED'; + $currentTask->saveOrFail(); + return $this->newTask; } @@ -143,6 +147,17 @@ private function rollbackToScriptTask() $this->addComment(); } + private function rollbackToServiceTask() + { + $this->copyTask(); + + $bpmnTask = $this->processDefinitions->getEvent($this->newTask->element_id); + + WorkflowManager::runServiceTask($bpmnTask, $this->newTask); + + $this->addComment(); + } + private function copyTask() { $newTask = $this->rollbackToTask->replicate(); diff --git a/resources/views/requests/show.blade.php b/resources/views/requests/show.blade.php index 0d9ef4857e..9e16877584 100644 --- a/resources/views/requests/show.blade.php +++ b/resources/views/requests/show.blade.php @@ -318,7 +318,7 @@ class="d-inline-flex pull-left align-items-center" :input-data="requestBy" displ
  • {{ __('Rollback Request') }}
    {{ __('Rollback to task') }}: {{ $eligibleRollbackTask->element_name }} ({{ $eligibleRollbackTask->element_id }}) @@ -713,12 +713,16 @@ classStatusCard() { apiRequest ); }, - rollback(name) { + rollback(errorTaskId, rollbackToName) { ProcessMaker.confirmModal( this.$t('Confirm'), - this.$t('Are you sure you want to rollback to the task: ') + name, + this.$t('Are you sure you want to rollback to the task @{{name}}? Warning! This request will continue as the current published process version.', { name: rollbackToName }), 'default', - () => { console.log("foo") } + () => { + ProcessMaker.apiClient.post(`tasks/${errorTaskId}/rollback`).then(response => { + location.reload(); + }); + } ) }, getConfigurationComments() { diff --git a/tests/Feature/Api/TasksTest.php b/tests/Feature/Api/TasksTest.php index 8e3ad2fa09..d9a5ff28cb 100644 --- a/tests/Feature/Api/TasksTest.php +++ b/tests/Feature/Api/TasksTest.php @@ -4,6 +4,7 @@ use Carbon\Carbon; use Database\Seeders\PermissionSeeder; +use Facades\ProcessMaker\RollbackProcessRequest; use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Support\Facades\Notification; use ProcessMaker\Facades\WorkflowManager; @@ -14,6 +15,7 @@ use ProcessMaker\Models\ProcessNotificationSetting; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\ProcessTaskAssignment; use ProcessMaker\Models\Screen; use ProcessMaker\Models\User; use ProcessMaker\Notifications\ActivityActivatedNotification; @@ -705,4 +707,50 @@ public function testSelfServeNotifications() Notification::assertSentTo([$this->user], ActivityActivatedNotification::class); } + + public function testRollback() + { + $bpmn = file_get_contents(__DIR__ . '/../../Fixtures/rollback_test.bpmn'); + $bpmn = str_replace('[task_user_id]', $this->user->id, $bpmn); + $process = Process::factory()->create([ + 'bpmn' => $bpmn, + ]); + ProcessTaskAssignment::factory()->create([ + 'process_id' => $process->id, + 'process_task_id' => 'node_255', + 'assignment_id' => $this->user->id, + 'assignment_type' => 'ProcessMaker\Models\User', + ]); + $definitions = $process->getDefinitions(); + $startEvent = $definitions->getEvent('node_1'); + $request = WorkflowManager::triggerStartEvent($process, $startEvent, []); + + $formTask = $request->tokens()->where('element_id', 'node_255')->firstOrFail(); + + // Next task should fail because the rule expression variable 'foo' does not exist + WorkflowManager::completeTask($process, $request, $formTask, ['someValue' => 123]); + + $this->assertEquals('ERROR', $request->refresh()->status); + + $errorTask = RollbackProcessRequest::getErrorTask($request); + $processDefinitions = $process->getDefinitions(); + $newTask = RollbackProcessRequest::rollback($errorTask, $processDefinitions); + + $this->assertEquals('ACTIVE', $request->refresh()->status); + $this->assertEquals($newTask->id, $request->tokens()->where('status', 'ACTIVE')->first()->id); + + // Set data to make the rule expression pass + WorkflowManager::completeTask($process, $request, $newTask, ['foo' => 123]); + + // Now we have a valid task we can use to complete the request + $ruleExpressionTask = $request->refresh()->tokens() + ->where('element_id', 'node_272') + ->where('status', 'ACTIVE') + ->firstOrFail(); + + WorkflowManager::completeTask($process, $request, $ruleExpressionTask, []); + + $this->assertEquals('CLOSED', $newTask->refresh()->status); + $this->assertEquals('COMPLETED', $request->refresh()->status); + } } diff --git a/tests/Fixtures/rollback_test.bpmn b/tests/Fixtures/rollback_test.bpmn new file mode 100644 index 0000000000..78bbc79c1a --- /dev/null +++ b/tests/Fixtures/rollback_test.bpmn @@ -0,0 +1,50 @@ + + + + + node_270 + + + node_283 + + + node_270 + node_281 + + + + node_281 + node_283 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php index b240c65643..4fc2a2d717 100644 --- a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php +++ b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php @@ -14,6 +14,7 @@ use ProcessMaker\Models\Script; use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Bpmn\ScriptTaskInterface; +use ProcessMaker\Nayra\Contracts\Bpmn\ServiceTaskInterface; use ProcessMaker\Repositories\BpmnDocument; class RollbackProcessRequestTest extends TestCase @@ -40,8 +41,7 @@ public function testRollbackToFormTask() 'element_type' => 'scriptTask', ]); - $mockProcessDefinitions = $this->mockWorkflowManager(); - + $mockProcessDefinitions = Mockery::mock(BpmnDocument::class); $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); $this->assertEquals('node_5', $newTask->element_id); $this->assertEquals('ACTIVE', $newTask->status); @@ -72,14 +72,44 @@ public function testRollbackToScriptTask() 'element_type' => 'scriptTask', ]); - $mockProcessDefinitions = $this->mockWorkflowManager(); + $mockProcessDefinitions = $this->mockRunScriptTask(); $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); $this->assertEquals('node_5', $newTask->element_id); $this->assertEquals('ACTIVE', $newTask->status); } - private function mockWorkflowManager() + public function testRollbackToServiceTask() + { + $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); + $task1 = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_5', + 'element_type' => 'serviceTask', + ]); + $task2 = ProcessRequestToken::factory()->create([ + 'status' => 'CLOSED', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_6', + 'element_type' => 'gateway', + ]); + $task3 = ProcessRequestToken::factory()->create([ + 'status' => 'FAILING', + 'process_request_id' => $processRequest->id, + 'element_id' => 'node_7', + 'element_type' => 'scriptTask', + ]); + + $mockProcessDefinitions = $this->mockRunServiceTask(); + + $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); + $this->assertEquals('node_5', $newTask->element_id); + $this->assertEquals('ACTIVE', $newTask->status); + $this->assertEquals('ACTIVE', $processRequest->refresh()->status); + } + + private function mockRunScriptTask() { $mocksScriptTask = Mockery::mock(ScriptTaskInterface::class); $mockProcessDefinitions = Mockery::mock(BpmnDocument::class); @@ -87,11 +117,25 @@ private function mockWorkflowManager() ->with('node_5') ->andReturn($mocksScriptTask); WorkflowManager::shouldReceive('runScripTask') - ->zeroOrMoreTimes() ->withArgs(function ($scriptTask, $task) use ($mocksScriptTask) { return $scriptTask === $mocksScriptTask && $task->element_id = 'node_5'; }); return $mockProcessDefinitions; } + + private function mockRunServiceTask() + { + $mockServiceTask = Mockery::mock(ServiceTaskInterface::class); + $mockProcessDefinitions = Mockery::mock(BpmnDocument::class); + $mockProcessDefinitions->shouldReceive('getEvent') + ->with('node_5') + ->andReturn($mockServiceTask); + WorkflowManager::shouldReceive('runServiceTask') + ->withArgs(function ($serviceTask, $task) use ($mockServiceTask) { + return $serviceTask === $mockServiceTask && $task->element_id = 'node_5'; + }); + + return $mockProcessDefinitions; + } } From 7f322ebbf14264caac0e1da4d845ea3e8c373252 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 12 Jul 2023 18:18:53 -0700 Subject: [PATCH 4/8] Allow rollback from gateway error --- ProcessMaker/RollbackProcessRequest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php index ae782a22f4..3be34d547f 100644 --- a/ProcessMaker/RollbackProcessRequest.php +++ b/ProcessMaker/RollbackProcessRequest.php @@ -58,10 +58,20 @@ class RollbackProcessRequest public function getErrorTask(ProcessRequest $processRequest) : ?ProcessRequestToken { $lastTask = $processRequest->tokens()->orderBy('id', 'desc')->first(); + if ($lastTask->status === 'FAILING') { return $lastTask; } + // Allow for gateway tasks + if ( + $lastTask->element_type === 'gateway' && + $lastTask->status === 'CLOSED' && + $processRequest->status === 'ERROR' + ) { + return $lastTask; + } + return null; } From 6830d1967bd647b49373226220f330c1307e65b2 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Wed, 12 Jul 2023 20:00:32 -0700 Subject: [PATCH 5/8] Return null if no tasks --- ProcessMaker/RollbackProcessRequest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php index 3be34d547f..a0ac68b9dc 100644 --- a/ProcessMaker/RollbackProcessRequest.php +++ b/ProcessMaker/RollbackProcessRequest.php @@ -59,6 +59,10 @@ public function getErrorTask(ProcessRequest $processRequest) : ?ProcessRequestTo { $lastTask = $processRequest->tokens()->orderBy('id', 'desc')->first(); + if (!$lastTask) { + return null; + } + if ($lastTask->status === 'FAILING') { return $lastTask; } From bd00969a0a5584496e4cea9de81b1c92a0065688 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Fri, 14 Jul 2023 11:02:23 -0700 Subject: [PATCH 6/8] Refactor test --- .../RollbackProcessRequestTest.php | 81 +++++++------------ 1 file changed, 27 insertions(+), 54 deletions(-) diff --git a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php index 4fc2a2d717..754ee5307a 100644 --- a/tests/unit/ProcessMaker/RollbackProcessRequestTest.php +++ b/tests/unit/ProcessMaker/RollbackProcessRequestTest.php @@ -19,94 +19,67 @@ class RollbackProcessRequestTest extends TestCase { - public function testRollbackToFormTask() + public $processRequest; + + public $rollbackToTask; + + public function createTasks($rollbackToType) { - $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); - $task1 = ProcessRequestToken::factory()->create([ + $this->processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); + $this->rollbackToTask = ProcessRequestToken::factory()->create([ 'status' => 'CLOSED', - 'process_request_id' => $processRequest->id, + 'process_request_id' => $this->processRequest->id, 'element_id' => 'node_5', - 'element_type' => 'task', + 'element_type' => $rollbackToType, ]); - $task2 = ProcessRequestToken::factory()->create([ + ProcessRequestToken::factory()->create([ 'status' => 'CLOSED', - 'process_request_id' => $processRequest->id, + 'process_request_id' => $this->processRequest->id, 'element_id' => 'node_6', 'element_type' => 'gateway', ]); - $task3 = ProcessRequestToken::factory()->create([ + $task = ProcessRequestToken::factory()->create([ 'status' => 'FAILING', - 'process_request_id' => $processRequest->id, + 'process_request_id' => $this->processRequest->id, 'element_id' => 'node_7', 'element_type' => 'scriptTask', ]); + return $task; + } + + public function testRollbackToFormTask() + { + $task = $this->createTasks('task'); + $mockProcessDefinitions = Mockery::mock(BpmnDocument::class); - $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); + $newTask = RollbackProcessRequest::rollback($task, $mockProcessDefinitions); $this->assertEquals('node_5', $newTask->element_id); $this->assertEquals('ACTIVE', $newTask->status); $comment = Comment::orderBy('id', 'desc')->first(); - $this->assertEquals($comment->body, "The System rolled back {$task3->element_name} to {$newTask->element_name}"); + $this->assertEquals($comment->body, "The System rolled back {$task->element_name} to {$newTask->element_name}"); } public function testRollbackToScriptTask() { - $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); - $task1 = ProcessRequestToken::factory()->create([ - 'status' => 'CLOSED', - 'process_request_id' => $processRequest->id, - 'element_id' => 'node_5', - 'element_type' => 'scriptTask', - ]); - $task2 = ProcessRequestToken::factory()->create([ - 'status' => 'CLOSED', - 'process_request_id' => $processRequest->id, - 'element_id' => 'node_6', - 'element_type' => 'gateway', - ]); - $task3 = ProcessRequestToken::factory()->create([ - 'status' => 'FAILING', - 'process_request_id' => $processRequest->id, - 'element_id' => 'node_7', - 'element_type' => 'scriptTask', - ]); - + $task = $this->createTasks('scriptTask'); $mockProcessDefinitions = $this->mockRunScriptTask(); - $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); + $newTask = RollbackProcessRequest::rollback($task, $mockProcessDefinitions); $this->assertEquals('node_5', $newTask->element_id); $this->assertEquals('ACTIVE', $newTask->status); } public function testRollbackToServiceTask() { - $processRequest = ProcessRequest::factory()->create(['status' => 'ERROR']); - $task1 = ProcessRequestToken::factory()->create([ - 'status' => 'CLOSED', - 'process_request_id' => $processRequest->id, - 'element_id' => 'node_5', - 'element_type' => 'serviceTask', - ]); - $task2 = ProcessRequestToken::factory()->create([ - 'status' => 'CLOSED', - 'process_request_id' => $processRequest->id, - 'element_id' => 'node_6', - 'element_type' => 'gateway', - ]); - $task3 = ProcessRequestToken::factory()->create([ - 'status' => 'FAILING', - 'process_request_id' => $processRequest->id, - 'element_id' => 'node_7', - 'element_type' => 'scriptTask', - ]); - + $task = $this->createTasks('serviceTask'); $mockProcessDefinitions = $this->mockRunServiceTask(); - $newTask = RollbackProcessRequest::rollback($task3, $mockProcessDefinitions); + $newTask = RollbackProcessRequest::rollback($task, $mockProcessDefinitions); $this->assertEquals('node_5', $newTask->element_id); $this->assertEquals('ACTIVE', $newTask->status); - $this->assertEquals('ACTIVE', $processRequest->refresh()->status); + $this->assertEquals('ACTIVE', $this->processRequest->refresh()->status); } private function mockRunScriptTask() From 571c55ad2876201a05314266fb6f3873aaaf2c43 Mon Sep 17 00:00:00 2001 From: Nolan Ehrstrom Date: Fri, 14 Jul 2023 13:45:45 -0700 Subject: [PATCH 7/8] Update rollback policy --- .../Policies/ProcessRequestTokenPolicy.php | 14 ++++++++++++++ resources/views/requests/show.blade.php | 2 +- routes/api.php | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ProcessMaker/Policies/ProcessRequestTokenPolicy.php b/ProcessMaker/Policies/ProcessRequestTokenPolicy.php index 092de5c7dc..5cdac4ed12 100644 --- a/ProcessMaker/Policies/ProcessRequestTokenPolicy.php +++ b/ProcessMaker/Policies/ProcessRequestTokenPolicy.php @@ -88,4 +88,18 @@ public function viewScreen(User $user, ProcessRequestToken $task, Screen $screen return true; } + + /** + * Determine if a user can rollback the process request. + * + * @param \ProcessMaker\Models\User $user + * @param \ProcessMaker\Models\ProcessRequest $processRequest + * + * @return bool + */ + public function rollback(User $user, ProcessRequestToken $task) + { + // For now, only the process manager can rollback the request + return $user->id === $task->process->managerId; + } } diff --git a/resources/views/requests/show.blade.php b/resources/views/requests/show.blade.php index 9e16877584..ada5a7fd0a 100644 --- a/resources/views/requests/show.blade.php +++ b/resources/views/requests/show.blade.php @@ -314,7 +314,7 @@ class="d-inline-flex pull-left align-items-center" :input-data="requestBy" displ
  • @endif @if ($eligibleRollbackTask) - @can('update', $request) + @can('rollback', $errorTask)
  • {{ __('Rollback Request') }}