diff --git a/ProcessMaker/Jobs/GenerateUserRecommendations.php b/ProcessMaker/Jobs/GenerateUserRecommendations.php index ba9d1629d8..024f3d92b0 100644 --- a/ProcessMaker/Jobs/GenerateUserRecommendations.php +++ b/ProcessMaker/Jobs/GenerateUserRecommendations.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Jobs; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -12,10 +13,15 @@ use ProcessMaker\Models\User; use ProcessMaker\RecommendationEngine; -class GenerateUserRecommendations implements ShouldQueue +class GenerateUserRecommendations implements ShouldQueue, ShouldBeUnique { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * Seconds the unique lock is held so duplicate dispatches for the same user are dropped. + */ + public int $uniqueFor = 60; + /** * Create a new job instance. */ @@ -23,6 +29,11 @@ public function __construct(public int $user_id) { } + public function uniqueId(): string + { + return (string) $this->user_id; + } + public function middleware(): array { return [(new WithoutOverlapping($this->user_id))->dontRelease()]; diff --git a/ProcessMaker/Models/ProcessRequest.php b/ProcessMaker/Models/ProcessRequest.php index 8e19f4efb1..b9e8d91edd 100644 --- a/ProcessMaker/Models/ProcessRequest.php +++ b/ProcessMaker/Models/ProcessRequest.php @@ -184,6 +184,28 @@ public function shouldBeSearchable() } } + /** + * Determine if the model existed in the search index prior to an update. + * Prevents unnecessary RemoveFromSearch jobs when indexed search is disabled. + * + * @return bool + */ + public function wasSearchableBeforeUpdate() + { + return $this->shouldBeSearchable(); + } + + /** + * Determine if the model existed in the search index prior to deletion. + * Prevents unnecessary RemoveFromSearch jobs when indexed search is disabled. + * + * @return bool + */ + public function wasSearchableBeforeDelete() + { + return $this->shouldBeSearchable(); + } + /** * Get the indexable data array for the model. * diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index b65c1b0a06..6a6198b7cf 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -202,6 +202,28 @@ public function shouldBeSearchable() } } + /** + * Determine if the model existed in the search index prior to an update. + * Prevents unnecessary RemoveFromSearch jobs when indexed search is disabled. + * + * @return bool + */ + public function wasSearchableBeforeUpdate() + { + return $this->shouldBeSearchable(); + } + + /** + * Determine if the model existed in the search index prior to deletion. + * Prevents unnecessary RemoveFromSearch jobs when indexed search is disabled. + * + * @return bool + */ + public function wasSearchableBeforeDelete() + { + return $this->shouldBeSearchable(); + } + /** * Boot application as a process instance. * diff --git a/tests/Feature/Jobs/GenerateUserRecommendationsUniqueTest.php b/tests/Feature/Jobs/GenerateUserRecommendationsUniqueTest.php new file mode 100644 index 0000000000..14bbaec52a --- /dev/null +++ b/tests/Feature/Jobs/GenerateUserRecommendationsUniqueTest.php @@ -0,0 +1,48 @@ +create(['status' => 'ACTIVE']); + + GenerateUserRecommendations::dispatch($user->id); + GenerateUserRecommendations::dispatch($user->id)->onQueue('low'); + + Queue::assertPushed(GenerateUserRecommendations::class, 1); + } + + public function test_smart_inbox_and_activity_completed_do_not_enqueue_twice_for_the_same_user(): void + { + Queue::fake(); + + $user = User::factory()->create(['status' => 'ACTIVE']); + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $user->id, + 'element_type' => 'task', + 'status' => 'CLOSED', + ]); + + (new SmartInbox($token->id))->handle(); + event(new ActivityCompleted($token)); + + Queue::assertPushed(GenerateUserRecommendations::class, 1); + Queue::assertPushed(GenerateUserRecommendations::class, function (GenerateUserRecommendations $job) use ($user) { + return $job->user_id === $user->id; + }); + } +} diff --git a/tests/Feature/Models/IndexedSearchScoutSyncTest.php b/tests/Feature/Models/IndexedSearchScoutSyncTest.php new file mode 100644 index 0000000000..57dbb5b257 --- /dev/null +++ b/tests/Feature/Models/IndexedSearchScoutSyncTest.php @@ -0,0 +1,124 @@ + true]); + } + + private function setIndexedSearch(bool $enabled): void + { + $setting = Setting::updateOrCreate( + ['key' => 'indexed-search'], + ['config' => ['enabled' => $enabled]] + ); + + $settingCache = SettingCacheFactory::getSettingsCache(); + $settingKey = $settingCache->createKey(['key' => 'indexed-search']); + $settingCache->set($settingKey, $setting->fresh()); + } + + public function test_was_searchable_hooks_follow_indexed_search_setting(): void + { + $this->setIndexedSearch(false); + $request = ProcessRequest::factory()->create(); + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $request->id, + 'element_type' => 'task', + ]); + + $this->assertFalse($request->shouldBeSearchable()); + $this->assertFalse($request->wasSearchableBeforeUpdate()); + $this->assertFalse($request->wasSearchableBeforeDelete()); + $this->assertFalse($token->shouldBeSearchable()); + $this->assertFalse($token->wasSearchableBeforeUpdate()); + $this->assertFalse($token->wasSearchableBeforeDelete()); + + $this->setIndexedSearch(true); + + $this->assertTrue($request->shouldBeSearchable()); + $this->assertTrue($request->wasSearchableBeforeUpdate()); + $this->assertTrue($request->wasSearchableBeforeDelete()); + $this->assertTrue($token->fresh()->shouldBeSearchable()); + $this->assertTrue($token->fresh()->wasSearchableBeforeUpdate()); + $this->assertTrue($token->fresh()->wasSearchableBeforeDelete()); + } + + public function test_save_does_not_dispatch_remove_from_search_when_indexed_search_is_disabled(): void + { + $this->setIndexedSearch(false); + $request = ProcessRequest::factory()->create(); + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $request->id, + 'element_type' => 'task', + ]); + + Queue::fake(); + + $request->name = 'Updated request'; + $request->save(); + $token->element_name = 'Updated token'; + $token->save(); + + Queue::assertNotPushed(RemoveFromSearch::class); + Queue::assertNotPushed(MakeSearchable::class); + } + + public function test_save_indexes_when_indexed_search_is_enabled(): void + { + $this->setIndexedSearch(true); + $request = ProcessRequest::factory()->create(); + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $request->id, + 'element_type' => 'task', + ]); + + Queue::fake(); + + $request->name = 'Searchable request'; + $request->save(); + $token->element_name = 'Searchable token'; + $token->save(); + + Queue::assertPushed(MakeSearchable::class); + Queue::assertNotPushed(RemoveFromSearch::class); + } + + public function test_delete_still_removes_from_search_when_indexed_search_is_enabled(): void + { + $this->setIndexedSearch(true); + $request = ProcessRequest::factory()->create(); + + Queue::fake(); + $request->delete(); + + Queue::assertPushed(RemoveFromSearch::class); + } + + public function test_delete_does_not_dispatch_remove_from_search_when_indexed_search_is_disabled(): void + { + $this->setIndexedSearch(false); + $request = ProcessRequest::factory()->create(); + + Queue::fake(); + $request->delete(); + + Queue::assertNotPushed(RemoveFromSearch::class); + } +}