Commit | Line | Data |
---|---|---|
e961651e CW |
1 | <?php |
2 | namespace api\v4\SearchDisplay; | |
3 | ||
5623bf2a | 4 | use Civi\API\Exception\UnauthorizedException; |
4775ec6f | 5 | use Civi\Api4\Activity; |
e961651e | 6 | use Civi\Api4\Contact; |
84b531d8 | 7 | use Civi\Api4\ContactType; |
f28a6f18 | 8 | use Civi\Api4\Email; |
faa79dbf CW |
9 | use Civi\Api4\SavedSearch; |
10 | use Civi\Api4\SearchDisplay; | |
11 | use Civi\Api4\UFMatch; | |
e961651e CW |
12 | use Civi\Test\HeadlessInterface; |
13 | use Civi\Test\TransactionalInterface; | |
14 | ||
15 | /** | |
16 | * @group headless | |
17 | */ | |
18 | class 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 | } |