Merge pull request #23016 from pradpnayak/optionValue
[civicrm-core.git] / ext / search_kit / tests / phpunit / api / v4 / SearchDisplay / SearchRunTest.php
CommitLineData
e961651e
CW
1<?php
2namespace api\v4\SearchDisplay;
3
5623bf2a 4use Civi\API\Exception\UnauthorizedException;
4775ec6f 5use Civi\Api4\Activity;
e961651e 6use Civi\Api4\Contact;
84b531d8 7use Civi\Api4\ContactType;
f28a6f18 8use Civi\Api4\Email;
faa79dbf
CW
9use Civi\Api4\SavedSearch;
10use Civi\Api4\SearchDisplay;
11use Civi\Api4\UFMatch;
e961651e
CW
12use Civi\Test\HeadlessInterface;
13use Civi\Test\TransactionalInterface;
14
15/**
16 * @group headless
17 */
18class SearchRunTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
57a4d21c 19 use \Civi\Test\ACLPermissionTrait;
e961651e
CW
20
21 public function setUpHeadless() {
22 // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
23 // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
24 return \Civi\Test::headless()
25 ->installMe(__DIR__)
26 ->apply();
27 }
28
e961651e
CW
29 /**
30 * Test running a searchDisplay with various filters.
31 */
c1335819 32 public function testRunWithFilters() {
84b531d8
CW
33 foreach (['Tester', 'Bot'] as $type) {
34 ContactType::create(FALSE)
35 ->addValue('parent_id.name', 'Individual')
36 ->addValue('label', $type)
37 ->addValue('name', $type)
38 ->execute();
39 }
40
e961651e
CW
41 $lastName = uniqid(__FUNCTION__);
42 $sampleData = [
84b531d8
CW
43 ['first_name' => 'One', 'last_name' => $lastName, 'contact_sub_type' => ['Tester', 'Bot']],
44 ['first_name' => 'Two', 'last_name' => $lastName, 'contact_sub_type' => ['Tester']],
45 ['first_name' => 'Three', 'last_name' => $lastName, 'contact_sub_type' => ['Bot']],
6cc91745 46 ['first_name' => 'Four', 'middle_name' => 'None', 'last_name' => $lastName],
e961651e
CW
47 ];
48 Contact::save(FALSE)->setRecords($sampleData)->execute();
49
50 $params = [
51 'checkPermissions' => FALSE,
52 'return' => 'page:1',
53 'savedSearch' => [
54 'api_entity' => 'Contact',
55 'api_params' => [
56 'version' => 4,
6cc91745 57 'select' => ['id', 'first_name', 'middle_name', 'last_name', 'contact_sub_type:label', 'is_deceased'],
e961651e
CW
58 'where' => [],
59 ],
60 ],
61 'display' => [
62 'type' => 'table',
63 'label' => '',
64 'settings' => [
65 'limit' => 20,
66 'pager' => TRUE,
67 'columns' => [
68 [
69 'key' => 'id',
70 'label' => 'Contact ID',
71 'dataType' => 'Integer',
72 'type' => 'field',
73 ],
74 [
75 'key' => 'first_name',
76 'label' => 'First Name',
77 'dataType' => 'String',
78 'type' => 'field',
79 ],
80 [
81 'key' => 'last_name',
82 'label' => 'Last Name',
83 'dataType' => 'String',
84 'type' => 'field',
85 ],
84b531d8
CW
86 [
87 'key' => 'contact_sub_type:label',
88 'label' => 'Type',
89 'dataType' => 'String',
90 'type' => 'field',
91 ],
8fd58f56
CW
92 [
93 'key' => 'is_deceased',
94 'label' => 'Deceased',
95 'dataType' => 'Boolean',
96 'type' => 'field',
97 ],
e961651e
CW
98 ],
99 'sort' => [
100 ['id', 'ASC'],
101 ],
102 ],
103 ],
104 'filters' => ['last_name' => $lastName],
105 'afform' => NULL,
106 ];
107
108 $result = civicrm_api4('SearchDisplay', 'run', $params);
109 $this->assertCount(4, $result);
110
111 $params['filters']['first_name'] = ['One', 'Two'];
112 $result = civicrm_api4('SearchDisplay', 'run', $params);
113 $this->assertCount(2, $result);
8fd58f56
CW
114 $this->assertEquals('One', $result[0]['data']['first_name']);
115 $this->assertEquals('Two', $result[1]['data']['first_name']);
7193d9f6
CW
116 $count = civicrm_api4('SearchDisplay', 'run', ['return' => 'row_count'] + $params);
117 $this->assertCount(2, $count);
e961651e 118
1fd2aa71 119 // Raw value should be boolean, view value should be string
8fd58f56
CW
120 $this->assertEquals(FALSE, $result[0]['data']['is_deceased']);
121 $this->assertEquals(ts('No'), $result[0]['columns'][4]['val']);
1fd2aa71 122
8fd58f56 123 $params['filters'] = ['last_name' => $lastName, 'id' => ['>' => $result[0]['data']['id'], '<=' => $result[1]['data']['id'] + 1]];
e961651e
CW
124 $params['sort'] = [['first_name', 'ASC']];
125 $result = civicrm_api4('SearchDisplay', 'run', $params);
126 $this->assertCount(2, $result);
8fd58f56
CW
127 $this->assertEquals('Three', $result[0]['data']['first_name']);
128 $this->assertEquals('Two', $result[1]['data']['first_name']);
7193d9f6
CW
129 $count = civicrm_api4('SearchDisplay', 'run', ['return' => 'row_count'] + $params);
130 $this->assertCount(2, $count);
84b531d8 131
6cc91745 132 $params['filters'] = ['last_name' => $lastName, 'contact_sub_type:label' => ['Tester', 'Bot']];
84b531d8
CW
133 $result = civicrm_api4('SearchDisplay', 'run', $params);
134 $this->assertCount(3, $result);
7193d9f6
CW
135 $count = civicrm_api4('SearchDisplay', 'run', ['return' => 'row_count'] + $params);
136 $this->assertCount(3, $count);
84b531d8 137
6cc91745
CW
138 // Comma indicates first_name OR last_name
139 $params['filters'] = ['first_name,last_name' => $lastName, 'contact_sub_type' => ['Tester']];
140 $result = civicrm_api4('SearchDisplay', 'run', $params);
141 $this->assertCount(2, $result);
7193d9f6
CW
142 $count = civicrm_api4('SearchDisplay', 'run', ['return' => 'row_count'] + $params);
143 $this->assertCount(2, $count);
6cc91745
CW
144
145 // Comma indicates first_name OR middle_name, matches "One" or "None"
146 $params['filters'] = ['first_name,middle_name' => 'one', 'last_name' => $lastName];
84b531d8
CW
147 $result = civicrm_api4('SearchDisplay', 'run', $params);
148 $this->assertCount(2, $result);
7193d9f6
CW
149 $count = civicrm_api4('SearchDisplay', 'run', ['return' => 'row_count'] + $params);
150 $this->assertCount(2, $count);
e961651e
CW
151 }
152
c1335819
CW
153 /**
154 * Test return values are augmented by tokens.
155 */
156 public function testWithTokens() {
157 $lastName = uniqid(__FUNCTION__);
158 $sampleData = [
159 ['first_name' => 'One', 'last_name' => $lastName, 'source' => 'Unit test'],
160 ['first_name' => 'Two', 'last_name' => $lastName, 'source' => 'Unit test'],
161 ];
162 Contact::save(FALSE)->setRecords($sampleData)->execute();
163
164 $params = [
165 'checkPermissions' => FALSE,
166 'return' => 'page:1',
167 'savedSearch' => [
168 'api_entity' => 'Contact',
169 'api_params' => [
170 'version' => 4,
171 'select' => ['id', 'display_name'],
172 'where' => [['last_name', '=', $lastName]],
173 ],
174 ],
175 'display' => [
176 'type' => 'table',
177 'label' => '',
178 'settings' => [
179 'limit' => 20,
180 'pager' => TRUE,
181 'columns' => [
182 [
183 'key' => 'id',
184 'label' => 'Contact ID',
185 'dataType' => 'Integer',
186 'type' => 'field',
187 ],
188 [
189 'key' => 'display_name',
190 'label' => 'Display Name',
191 'dataType' => 'String',
192 'type' => 'field',
193 'link' => [
194 'path' => 'civicrm/test/token-[sort_name]',
195 ],
196 ],
197 ],
198 'sort' => [
199 ['id', 'ASC'],
200 ],
201 ],
202 ],
203 ];
204
205 $result = civicrm_api4('SearchDisplay', 'run', $params);
206 $this->assertCount(2, $result);
8fd58f56 207 $this->assertNotEmpty($result->first()['data']['display_name']);
c1335819 208 // Assert that display name was added to the search due to the link token
8fd58f56 209 $this->assertNotEmpty($result->first()['data']['sort_name']);
c1335819
CW
210
211 // These items are not part of the search, but will be added via links
7616c89d
CW
212 $this->assertArrayNotHasKey('contact_type', $result->first()['data']);
213 $this->assertArrayNotHasKey('source', $result->first()['data']);
214 $this->assertArrayNotHasKey('last_name', $result->first()['data']);
c1335819
CW
215
216 // Add links
217 $params['display']['settings']['columns'][] = [
218 'type' => 'links',
219 'label' => 'Links',
220 'links' => [
221 ['path' => 'civicrm/test-[source]-[contact_type]'],
222 ['path' => 'civicrm/test-[last_name]'],
223 ],
224 ];
225 $result = civicrm_api4('SearchDisplay', 'run', $params);
8fd58f56
CW
226 $this->assertEquals('Individual', $result->first()['data']['contact_type']);
227 $this->assertEquals('Unit test', $result->first()['data']['source']);
228 $this->assertEquals($lastName, $result->first()['data']['last_name']);
c1335819
CW
229 }
230
7616c89d
CW
231 /**
232 * Test smarty rewrite syntax.
233 */
234 public function testRunWithSmartyRewrite() {
235 $lastName = uniqid(__FUNCTION__);
236 $sampleData = [
237 ['first_name' => 'One', 'last_name' => $lastName, 'nick_name' => 'Uno'],
238 ['first_name' => 'Two', 'last_name' => $lastName],
239 ];
240 $contacts = Contact::save(FALSE)->setRecords($sampleData)->execute();
241 Email::create(FALSE)
242 ->addValue('contact_id', $contacts[0]['id'])
243 ->addValue('email', 'testmail@unit.test')
244 ->execute();
245
246 $params = [
247 'checkPermissions' => FALSE,
248 'return' => 'page:1',
249 'savedSearch' => [
250 'api_entity' => 'Contact',
251 'api_params' => [
252 'version' => 4,
253 'select' => ['id', 'first_name', 'last_name', 'nick_name', 'Contact_Email_contact_id_01.email', 'Contact_Email_contact_id_01.location_type_id:label'],
254 'where' => [['last_name', '=', $lastName]],
255 'join' => [
256 [
257 "Email AS Contact_Email_contact_id_01",
258 "LEFT",
259 ["id", "=", "Contact_Email_contact_id_01.contact_id"],
260 ["Contact_Email_contact_id_01.is_primary", "=", TRUE],
261 ],
262 ],
263 ],
264 ],
265 'display' => [
266 'type' => 'table',
267 'label' => '',
268 'settings' => [
269 'limit' => 20,
270 'pager' => TRUE,
271 'columns' => [
272 [
273 'key' => 'id',
274 'label' => 'Contact ID',
275 'type' => 'field',
276 ],
277 [
278 'key' => 'first_name',
279 'label' => 'Name',
280 'type' => 'field',
281 'rewrite' => '{if "[nick_name]"}[nick_name]{else}[first_name]{/if} [last_name]',
282 ],
283 [
284 'key' => 'Contact_Email_contact_id_01.email',
285 'label' => 'Email',
286 'type' => 'field',
287 'rewrite' => '{if "[Contact_Email_contact_id_01.email]"}[Contact_Email_contact_id_01.email] ([Contact_Email_contact_id_01.location_type_id:label]){/if}',
288 ],
289 ],
290 'sort' => [
291 ['id', 'ASC'],
292 ],
293 ],
294 ],
295 ];
296 $result = civicrm_api4('SearchDisplay', 'run', $params);
297 $this->assertEquals("Uno $lastName", $result[0]['columns'][1]['val']);
298 $this->assertEquals("Two $lastName", $result[1]['columns'][1]['val']);
299 $this->assertEquals("testmail@unit.test (Home)", $result[0]['columns'][2]['val']);
300 $this->assertEquals("", $result[1]['columns'][2]['val']);
301
302 // Try running it with illegal tags like {crmApi}
303 $params['display']['columns'][1]['rewrite'] = '{crmApi entity="Email" action="get" va="notAllowed"}';
304 try {
305 civicrm_api4('SearchDisplay', 'run', $params);
306 $this->fail();
307 }
308 catch (\Exception $e) {
309 }
310 }
311
faa79dbf
CW
312 /**
313 * Test running a searchDisplay as a restricted user.
314 */
315 public function testDisplayACLCheck() {
316 $lastName = uniqid(__FUNCTION__);
317 $sampleData = [
318 ['first_name' => 'User', 'last_name' => uniqid('user')],
319 ['first_name' => 'One', 'last_name' => $lastName],
320 ['first_name' => 'Two', 'last_name' => $lastName],
321 ['first_name' => 'Three', 'last_name' => $lastName],
322 ['first_name' => 'Four', 'last_name' => $lastName],
323 ];
324 $sampleData = Contact::save(FALSE)
325 ->setRecords($sampleData)->execute()
326 ->indexBy('first_name')->column('id');
327
328 // Create logged-in user
329 UFMatch::delete(FALSE)
330 ->addWhere('uf_id', '=', 6)
331 ->execute();
332 UFMatch::create(FALSE)->setValues([
333 'contact_id' => $sampleData['User'],
334 'uf_name' => 'superman',
335 'uf_id' => 6,
336 ])->execute();
337
338 $session = \CRM_Core_Session::singleton();
339 $session->set('userID', $sampleData['User']);
340 $hooks = \CRM_Utils_Hook::singleton();
341 \CRM_Core_Config::singleton()->userPermissionClass->permissions = [
342 'access CiviCRM',
343 ];
344
345 $search = SavedSearch::create(FALSE)
346 ->setValues([
347 'name' => uniqid(__FUNCTION__),
348 'api_entity' => 'Contact',
349 'api_params' => [
350 'version' => 4,
351 'select' => ['id', 'first_name', 'last_name'],
352 'where' => [['last_name', '=', $lastName]],
353 ],
354 ])
355 ->addChain('display', SearchDisplay::create()
356 ->setValues([
357 'type' => 'table',
358 'label' => uniqid(__FUNCTION__),
359 'saved_search_id' => '$id',
360 'settings' => [
361 'limit' => 20,
362 'pager' => TRUE,
363 'columns' => [
364 [
365 'key' => 'id',
366 'label' => 'Contact ID',
367 'dataType' => 'Integer',
368 'type' => 'field',
369 ],
370 [
371 'key' => 'first_name',
372 'label' => 'First Name',
373 'dataType' => 'String',
374 'type' => 'field',
375 ],
376 [
377 'key' => 'last_name',
378 'label' => 'Last Name',
379 'dataType' => 'String',
380 'type' => 'field',
381 ],
382 ],
383 'sort' => [
384 ['id', 'ASC'],
385 ],
386 ],
387 ]), 0)
388 ->execute()->first();
389
390 $params = [
391 'return' => 'page:1',
392 'savedSearch' => $search['name'],
393 'display' => $search['display']['name'],
394 'afform' => NULL,
395 ];
396
397 $hooks->setHook('civicrm_aclWhereClause', [$this, 'aclWhereHookNoResults']);
398 $result = civicrm_api4('SearchDisplay', 'run', $params);
399 $this->assertCount(0, $result);
400
401 $this->allowedContactId = $sampleData['Two'];
402 $hooks->setHook('civicrm_aclWhereClause', [$this, 'aclWhereOnlyOne']);
403 $this->cleanupCachedPermissions();
404 $result = civicrm_api4('SearchDisplay', 'run', $params);
405 $this->assertCount(1, $result);
8fd58f56 406 $this->assertEquals($sampleData['Two'], $result[0]['data']['id']);
faa79dbf
CW
407
408 $hooks->setHook('civicrm_aclWhereClause', [$this, 'aclWhereGreaterThan']);
409 $this->cleanupCachedPermissions();
410 $result = civicrm_api4('SearchDisplay', 'run', $params);
411 $this->assertCount(2, $result);
8fd58f56
CW
412 $this->assertEquals($sampleData['Three'], $result[0]['data']['id']);
413 $this->assertEquals($sampleData['Four'], $result[1]['data']['id']);
5623bf2a
CW
414 }
415
416 public function testWithACLBypass() {
417 $config = \CRM_Core_Config::singleton();
418 $config->userPermissionClass->permissions = ['all CiviCRM permissions and ACLs'];
419
420 $lastName = uniqid(__FUNCTION__);
421 $searchName = uniqid(__FUNCTION__);
422 $displayName = uniqid(__FUNCTION__);
423 $sampleData = [
424 ['first_name' => 'One', 'last_name' => $lastName],
425 ['first_name' => 'Two', 'last_name' => $lastName],
426 ['first_name' => 'Three', 'last_name' => $lastName],
427 ['first_name' => 'Four', 'last_name' => $lastName],
428 ];
429 Contact::save()->setRecords($sampleData)->execute();
430
431 // Super admin may create a display with acl_bypass
432 $search = SavedSearch::create()
433 ->setValues([
434 'name' => $searchName,
435 'title' => 'Test Saved Search',
436 'api_entity' => 'Contact',
437 'api_params' => [
438 'version' => 4,
439 'select' => ['id', 'first_name', 'last_name'],
440 'where' => [],
441 ],
442 ])
443 ->addChain('display', SearchDisplay::create()
444 ->setValues([
445 'saved_search_id' => '$id',
446 'name' => $displayName,
447 'type' => 'table',
448 'label' => '',
449 'acl_bypass' => TRUE,
450 'settings' => [
451 'limit' => 20,
452 'pager' => TRUE,
453 'columns' => [
454 [
455 'key' => 'id',
456 'label' => 'Contact ID',
457 'dataType' => 'Integer',
458 'type' => 'field',
459 ],
460 [
461 'key' => 'first_name',
462 'label' => 'First Name',
463 'dataType' => 'String',
464 'type' => 'field',
465 ],
466 [
467 'key' => 'last_name',
468 'label' => 'Last Name',
469 'dataType' => 'String',
470 'type' => 'field',
471 ],
472 ],
473 'sort' => [
474 ['id', 'ASC'],
475 ],
476 ],
477 ]))
478 ->execute()->first();
479
480 // Super admin may update a display with acl_bypass
481 SearchDisplay::update()->addWhere('name', '=', $displayName)
482 ->addValue('label', 'Test Display')
483 ->execute();
484
485 $config->userPermissionClass->permissions = ['administer CiviCRM'];
486 // Ordinary admin may not edit display because it has acl_bypass
487 $error = NULL;
488 try {
489 SearchDisplay::update()->addWhere('name', '=', $displayName)
490 ->addValue('label', 'Test Display')
491 ->execute();
492 }
493 catch (UnauthorizedException $e) {
494 $error = $e->getMessage();
495 }
496 $this->assertStringContainsString('failed', $error);
497
498 // Ordinary admin may not change the value of acl_bypass
499 $error = NULL;
500 try {
501 SearchDisplay::update()->addWhere('name', '=', $displayName)
502 ->addValue('acl_bypass', FALSE)
503 ->execute();
504 }
505 catch (UnauthorizedException $e) {
506 $error = $e->getMessage();
507 }
508 $this->assertStringContainsString('failed', $error);
509
510 // Ordinary admin may not edit the search because the display has acl_bypass
511 $error = NULL;
512 try {
513 SavedSearch::update()->addWhere('name', '=', $searchName)
514 ->addValue('title', 'Tested Search')
515 ->execute();
516 }
517 catch (UnauthorizedException $e) {
518 $error = $e->getMessage();
519 }
520 $this->assertStringContainsString('failed', $error);
521
522 $params = [
523 'checkPermissions' => FALSE,
524 'return' => 'page:1',
525 'savedSearch' => $searchName,
526 'display' => $displayName,
527 'filters' => ['last_name' => $lastName],
528 'afform' => NULL,
529 ];
530
531 $config->userPermissionClass->permissions = ['access CiviCRM'];
532
533 $result = civicrm_api4('SearchDisplay', 'run', $params);
534 $this->assertCount(4, $result);
535
536 $config->userPermissionClass->permissions = ['all CiviCRM permissions and ACLs'];
537 $params['checkPermissions'] = TRUE;
538
539 $result = civicrm_api4('SearchDisplay', 'run', $params);
540 $this->assertCount(4, $result);
541
542 $config->userPermissionClass->permissions = ['administer CiviCRM'];
543 $error = NULL;
544 try {
545 civicrm_api4('SearchDisplay', 'run', $params);
546 }
547 catch (UnauthorizedException $e) {
548 $error = $e->getMessage();
549 }
550 $this->assertStringContainsString('denied', $error);
551
552 $config->userPermissionClass->permissions = ['all CiviCRM permissions and ACLs'];
553
554 // Super users can update the acl_bypass field
555 SearchDisplay::update()->addWhere('name', '=', $displayName)
556 ->addValue('acl_bypass', FALSE)
557 ->execute();
558
559 $config->userPermissionClass->permissions = ['view all contacts'];
560 // And ordinary users can now run it
561 $result = civicrm_api4('SearchDisplay', 'run', $params);
562 $this->assertCount(4, $result);
563
564 // But not edit
565 $error = NULL;
566 try {
567 SearchDisplay::update()->addWhere('name', '=', $displayName)
568 ->addValue('label', 'Tested Display')
569 ->execute();
570 }
571 catch (UnauthorizedException $e) {
572 $error = $e->getMessage();
573 }
574 $this->assertStringContainsString('failed', $error);
575
144025ae 576 $config->userPermissionClass->permissions = ['access CiviCRM', 'administer CiviCRM data'];
5623bf2a
CW
577
578 // Admins can edit the search and the display
579 SavedSearch::update()->addWhere('name', '=', $searchName)
580 ->addValue('title', 'Tested Search')
581 ->execute();
582 SearchDisplay::update()->addWhere('name', '=', $displayName)
583 ->addValue('label', 'Tested Display')
584 ->execute();
faa79dbf 585
5623bf2a
CW
586 // But they can't edit the acl_bypass field
587 $error = NULL;
588 try {
589 SearchDisplay::update()->addWhere('name', '=', $displayName)
590 ->addValue('acl_bypass', TRUE)
591 ->execute();
592 }
593 catch (UnauthorizedException $e) {
594 $error = $e->getMessage();
595 }
596 $this->assertStringContainsString('failed', $error);
faa79dbf
CW
597 }
598
2eee3858
CW
599 /**
600 * Test running a searchDisplay with random sorting.
601 */
602 public function testSortByRand() {
603 $lastName = uniqid(__FUNCTION__);
604 $sampleData = [
605 ['first_name' => 'One', 'last_name' => $lastName],
606 ['first_name' => 'Two', 'last_name' => $lastName],
607 ['first_name' => 'Three', 'last_name' => $lastName],
608 ['first_name' => 'Four', 'last_name' => $lastName],
609 ];
610 Contact::save(FALSE)->setRecords($sampleData)->execute();
611
612 $params = [
613 'checkPermissions' => FALSE,
614 'return' => 'page:1',
615 'savedSearch' => [
616 'api_entity' => 'Contact',
617 'api_params' => [
618 'version' => 4,
619 'select' => ['id', 'first_name', 'last_name'],
620 'where' => [['last_name', '=', $lastName]],
621 ],
622 ],
623 'display' => [
624 'type' => 'list',
625 'label' => '',
626 'settings' => [
627 'limit' => 20,
628 'pager' => TRUE,
629 'columns' => [
630 [
631 'key' => 'first_name',
632 'label' => 'First Name',
633 'dataType' => 'String',
634 'type' => 'field',
635 ],
636 ],
637 'sort' => [
638 ['RAND()', 'ASC'],
639 ],
640 ],
641 ],
642 'afform' => NULL,
643 ];
644
645 // Without seed, results are returned in unpredictable order
646 // (hard to test this, but we can at least assert we get the correct number of results back)
647 $unseeded = civicrm_api4('SearchDisplay', 'run', $params);
648 $this->assertCount(4, $unseeded);
649
650 // Seed must be an integer
651 $params['seed'] = 'hello';
652 try {
653 civicrm_api4('SearchDisplay', 'run', $params);
654 $this->fail();
655 }
656 catch (\API_Exception $e) {
657 }
658
659 // With a random seed, results should be shuffled in stable order
660 $params['seed'] = 12345678987654321;
661 $seeded = civicrm_api4('SearchDisplay', 'run', $params);
662
663 // Same seed, same order every time
664 for ($i = 0; $i <= 9; ++$i) {
665 $repeat = civicrm_api4('SearchDisplay', 'run', $params);
f28a6f18 666 $this->assertEquals($seeded->column('data'), $repeat->column('data'));
2eee3858
CW
667 }
668 }
669
4775ec6f
CW
670 public function testRunWithGroupBy() {
671 Activity::delete(FALSE)
672 ->addWhere('activity_type_id:name', 'IN', ['Meeting', 'Phone Call'])
673 ->execute();
674
675 $cid = Contact::create(FALSE)
676 ->execute()->first()['id'];
677 $sampleData = [
678 ['subject' => 'abc', 'activity_type_id:name' => 'Meeting', 'source_contact_id' => $cid],
679 ['subject' => 'def', 'activity_type_id:name' => 'Meeting', 'source_contact_id' => $cid],
680 ['subject' => 'xyz', 'activity_type_id:name' => 'Phone Call', 'source_contact_id' => $cid],
681 ];
682 $aids = Activity::save(FALSE)
683 ->setRecords($sampleData)
684 ->execute()->column('id');
685
686 $params = [
687 'checkPermissions' => FALSE,
688 'return' => 'page:1',
689 'savedSearch' => [
690 'api_entity' => 'Activity',
691 'api_params' => [
692 'version' => 4,
693 'select' => [
694 "activity_type_id:label",
695 "GROUP_CONCAT(DISTINCT subject) AS GROUP_CONCAT_subject",
696 ],
697 'groupBy' => ['activity_type_id'],
698 'orderBy' => ['activity_type_id:label'],
699 'where' => [],
700 ],
701 ],
702 ];
703
704 $result = civicrm_api4('SearchDisplay', 'run', $params);
705
706 $this->assertEquals(['abc', 'def'], $result[0]['data']['GROUP_CONCAT_subject']);
707 $this->assertEquals(['xyz'], $result[1]['data']['GROUP_CONCAT_subject']);
708 }
709
f28a6f18
CW
710 /**
711 * Test conditional styles
712 */
713 public function testCssRules() {
714 $lastName = uniqid(__FUNCTION__);
715 $sampleContacts = [
716 ['first_name' => 'Zero', 'last_name' => $lastName, 'is_deceased' => TRUE],
717 ['first_name' => 'One', 'last_name' => $lastName],
718 ['first_name' => 'Two', 'last_name' => $lastName],
719 ['first_name' => 'Three', 'last_name' => $lastName],
720 ];
721 $contacts = Contact::save(FALSE)->setRecords($sampleContacts)->execute();
722 $sampleEmails = [
723 ['contact_id' => $contacts[0]['id'], 'email' => 'abc@123', 'on_hold' => 1],
724 ['contact_id' => $contacts[0]['id'], 'email' => 'def@123', 'on_hold' => 0],
725 ['contact_id' => $contacts[1]['id'], 'email' => 'ghi@123', 'on_hold' => 0],
726 ['contact_id' => $contacts[2]['id'], 'email' => 'jkl@123', 'on_hold' => 2],
727 ];
728 Email::save(FALSE)->setRecords($sampleEmails)->execute();
729
730 $search = [
731 'name' => 'Test',
732 'label' => 'Test Me',
733 'api_entity' => 'Contact',
734 'api_params' => [
735 'version' => 4,
736 'select' => [
737 'id',
738 'display_name',
739 'GROUP_CONCAT(DISTINCT Contact_Email_contact_id_01.email) AS GROUP_CONCAT_Contact_Email_contact_id_01_email',
740 ],
741 'where' => [['last_name', '=', $lastName]],
742 'groupBy' => ['id'],
743 'join' => [
744 [
745 'Email AS Contact_Email_contact_id_01',
746 'LEFT',
747 ['id', '=', 'Contact_Email_contact_id_01.contact_id'],
748 ],
749 ],
750 'having' => [],
751 ],
752 'acl_bypass' => FALSE,
753 ];
754
755 $display = [
756 'type' => 'table',
757 'settings' => [
758 'actions' => TRUE,
759 'limit' => 50,
760 'classes' => ['table', 'table-striped'],
761 'pager' => [
762 'show_count' => TRUE,
763 'expose_limit' => TRUE,
764 ],
765 'columns' => [
766 [
767 'type' => 'field',
768 'key' => 'id',
769 'dataType' => 'Integer',
770 'label' => 'Contact ID',
771 'sortable' => TRUE,
772 'alignment' => 'text-center',
773 ],
774 [
775 'type' => 'field',
776 'key' => 'display_name',
777 'dataType' => 'String',
778 'label' => 'Display Name',
779 'sortable' => TRUE,
780 'link' => [
781 'entity' => 'Contact',
782 'action' => 'view',
783 'target' => '_blank',
784 ],
785 'title' => 'View Contact',
786 ],
787 [
788 'type' => 'field',
789 'key' => 'GROUP_CONCAT_Contact_Email_contact_id_01_email',
790 'dataType' => 'String',
791 'label' => '(List) Contact Emails: Email',
792 'sortable' => TRUE,
793 'alignment' => 'text-right',
794 'cssRules' => [
795 [
796 'bg-danger',
79e6ec68 797 'Contact_Email_contact_id_01.on_hold:label',
f28a6f18
CW
798 '=',
799 'On Hold Bounce',
800 ],
801 [
802 'bg-warning',
79e6ec68 803 'Contact_Email_contact_id_01.on_hold:label',
f28a6f18
CW
804 '=',
805 'On Hold Opt Out',
806 ],
807 ],
808 'rewrite' => '',
809 'title' => NULL,
810 ],
811 ],
812 'cssRules' => [
813 ['strikethrough', 'is_deceased', '=', TRUE],
814 ],
815 ],
816 ];
817
818 $result = SearchDisplay::Run(FALSE)
819 ->setSavedSearch($search)
820 ->setDisplay($display)
821 ->setReturn('page:1')
822 ->setSort([['id', 'ASC']])
823 ->execute();
824
825 // Non-conditional style rule
826 $this->assertEquals('text-center', $result[0]['columns'][0]['cssClass']);
827 // First contact is deceased, gets strikethrough class
828 $this->assertEquals('strikethrough', $result[0]['cssClass']);
829 $this->assertNotEquals('strikethrough', $result[1]['cssClass']);
830 // Ensure the view contact link was formed
831 $this->assertStringContainsString('cid=' . $contacts[0]['id'], $result[0]['columns'][1]['links'][0]['url']);
832 $this->assertEquals('_blank', $result[0]['columns'][1]['links'][0]['target']);
833 // 1st column gets static + conditional style
834 $this->assertStringContainsString('text-right', $result[0]['columns'][2]['cssClass']);
835 $this->assertStringContainsString('bg-danger', $result[0]['columns'][2]['cssClass']);
836 // 2nd row gets static style but no conditional styles apply
837 $this->assertEquals('text-right', $result[1]['columns'][2]['cssClass']);
838 // 3rd column gets static + conditional style
839 $this->assertStringContainsString('text-right', $result[2]['columns'][2]['cssClass']);
840 $this->assertStringContainsString('bg-warning', $result[2]['columns'][2]['cssClass']);
841 }
842
cc7246dd 843 /**
dd2e872d
CW
844 * Test conditional and field-based icons
845 */
846 public function testIcons() {
847 $subject = uniqid(__FUNCTION__);
848
849 $source = Contact::create(FALSE)->execute()->first();
850
851 $activities = [
852 ['activity_type_id:name' => 'Meeting', 'subject' => $subject, 'status_id:name' => 'Scheduled'],
853 ['activity_type_id:name' => 'Phone Call', 'subject' => $subject, 'status_id:name' => 'Completed'],
854 ];
855 Activity::save(FALSE)
856 ->addDefault('source_contact_id', $source['id'])
857 ->setRecords($activities)->execute();
858
859 $search = [
860 'api_entity' => 'Activity',
861 'api_params' => [
862 'version' => 4,
863 'select' => [
864 'id',
865 ],
866 'orderBy' => [],
867 'where' => [],
868 'groupBy' => [],
869 'join' => [],
870 'having' => [],
871 ],
872 ];
873
874 $display = [
875 'type' => 'table',
876 'settings' => [
877 'actions' => TRUE,
878 'limit' => 50,
879 'classes' => [
880 'table',
881 'table-striped',
882 ],
883 'pager' => [
884 'show_count' => TRUE,
885 'expose_limit' => TRUE,
886 ],
887 'sort' => [],
888 'columns' => [
889 [
890 'type' => 'field',
891 'key' => 'id',
892 'dataType' => 'Integer',
893 'label' => 'Activity ID',
894 'sortable' => TRUE,
895 'icons' => [
896 [
897 'field' => 'activity_type_id:icon',
898 'side' => 'left',
899 ],
900 [
901 'icon' => 'fa-star',
902 'side' => 'right',
903 'if' => [
904 'status_id:name',
905 '=',
906 'Completed',
907 ],
908 ],
909 ],
910 ],
911 ],
912 ],
913 'acl_bypass' => FALSE,
914 ];
915
916 $result = SearchDisplay::Run(FALSE)
917 ->setSavedSearch($search)
918 ->setDisplay($display)
919 ->setReturn('page:1')
920 ->setSort([['id', 'ASC']])
921 ->execute();
922
923 // Icon based on activity type
924 $this->assertEquals([['class' => 'fa-slideshare', 'side' => 'left']], $result[0]['columns'][0]['icons']);
925 // Activity type icon + conditional icon based on status
926 $this->assertEquals([['class' => 'fa-phone', 'side' => 'left'], ['class' => 'fa-star', 'side' => 'right']], $result[1]['columns'][0]['icons']);
927 }
928
929 /**
930 * Test value substitutions with empty fields & placeholders
cc7246dd
CW
931 */
932 public function testPlaceholderFields() {
933 $lastName = uniqid(__FUNCTION__);
934 $sampleContacts = [
935 ['first_name' => 'Zero', 'last_name' => $lastName, 'nick_name' => 'Nick'],
936 ['first_name' => 'First', 'last_name' => $lastName],
937 ];
938 Contact::save(FALSE)->setRecords($sampleContacts)->execute();
939
940 $search = [
941 'name' => 'Test',
942 'label' => 'Test Me',
943 'api_entity' => 'Contact',
944 'api_params' => [
945 'version' => 4,
946 'select' => ['id', 'nick_name'],
947 'where' => [['last_name', '=', $lastName]],
948 ],
949 'acl_bypass' => FALSE,
950 ];
951
952 $display = [
953 'type' => 'table',
954 'settings' => [
955 'actions' => TRUE,
956 'columns' => [
957 [
958 'type' => 'field',
959 'key' => 'id',
960 'dataType' => 'Integer',
961 'label' => 'Contact ID',
962 'sortable' => TRUE,
963 'alignment' => 'text-center',
964 ],
965 [
966 'type' => 'field',
967 'key' => 'nick_name',
968 'dataType' => 'String',
969 'label' => 'Display Name',
970 'sortable' => TRUE,
971 'rewrite' => '[nick_name] [last_name]',
972 'empty_value' => '[first_name] [last_name]',
973 'link' => [
974 'entity' => 'Contact',
975 'action' => 'view',
976 'target' => '_blank',
977 ],
978 'title' => '[display_name]',
979 ],
980 ],
981 ],
982 ];
983
984 $result = SearchDisplay::Run(FALSE)
985 ->setSavedSearch($search)
986 ->setDisplay($display)
987 ->setReturn('page:1')
988 ->setSort([['id', 'ASC']])
989 ->execute();
990
991 // Has a nick name
992 $this->assertEquals("Nick $lastName", $result[0]['columns'][1]['val']);
993 $this->assertEquals("Nick $lastName", $result[0]['columns'][1]['links'][0]['text']);
994 // Title is display name
995 $this->assertEquals("Zero $lastName", $result[0]['columns'][1]['title']);
996 // No nick name - using first name instead per empty_value setting
997 $this->assertEquals("First $lastName", $result[1]['columns'][1]['val']);
998 $this->assertEquals("First $lastName", $result[1]['columns'][1]['title']);
999 $this->assertEquals("First $lastName", $result[1]['columns'][1]['links'][0]['text']);
1000 // Check links
1001 $this->assertNotEmpty($result[0]['columns'][1]['links'][0]['url']);
1002 $this->assertNotEmpty($result[1]['columns'][1]['links'][0]['url']);
1003 }
1004
49208bdb
CW
1005 /**
1006 * Ensure SearchKit can cope with a non-DAO-based entity
1007 */
1008 public function testRunWithNonDaoEntity() {
1009 $search = [
1010 'api_entity' => 'Entity',
1011 'api_params' => [
1012 'version' => 4,
1013 'select' => ['name'],
1014 'where' => [['name', '=', 'Contact']],
1015 ],
1016 ];
1017
1018 $display = [
1019 'type' => 'table',
1020 'settings' => [
1021 'actions' => TRUE,
1022 'columns' => [
1023 [
1024 'type' => 'field',
1025 'key' => 'name',
1026 'label' => 'Name',
1027 'sortable' => TRUE,
1028 ],
1029 ],
1030 ],
1031 ];
1032
1033 $result = SearchDisplay::Run(FALSE)
1034 ->setSavedSearch($search)
1035 ->setDisplay($display)
1036 ->setReturn('page:1')
1037 ->execute();
1038
1039 $this->assertCount(1, $result);
1040 $this->assertEquals('Contact', $result[0]['columns'][0]['val']);
1041 }
1042
e961651e 1043}