Search ext: Extend api4 smart groups to work with HAVING
authorColeman Watts <coleman@civicrm.org>
Wed, 30 Sep 2020 18:56:59 +0000 (14:56 -0400)
committerColeman Watts <coleman@civicrm.org>
Sat, 3 Oct 2020 00:42:22 +0000 (20:42 -0400)
CRM/Contact/BAO/GroupContactCache.php
CRM/Utils/API/HTMLInputCoder.php
ext/search/ang/search/SaveSmartGroup.ctrl.js
ext/search/ang/search/crmSearch.component.js
ext/search/ang/search/saveSmartGroup.html
tests/phpunit/api/v4/Entity/SavedSearchTest.php

index d4700184f81b9c8747df1e605611c459e1d50375..ce082757662b4767cac5c13d81e7bb46f6a9453e 100644 (file)
@@ -9,6 +9,8 @@
  +--------------------------------------------------------------------+
  */
 
+use Civi\Api4\Query\SqlExpression;
+
 /**
  *
  * @package CRM
@@ -701,16 +703,18 @@ ORDER BY   gc.contact_id, g.children
    */
   protected static function getApiSQL(array $savedSearch, string $addSelect, string $excludeClause) {
     $apiParams = $savedSearch['api_params'] + ['select' => ['id'], 'checkPermissions' => FALSE];
-    list($idField) = explode(' AS ', $apiParams['select'][0]);
-    $apiParams['select'] = [
-      $addSelect,
-      $idField,
-    ];
+    $idField = SqlExpression::convert($apiParams['select'][0], TRUE)->getAlias();
+    // Unless there's a HAVING clause, we don't care about other columns
+    if (empty($apiParams['having'])) {
+      $apiParams['select'] = array_slice($apiParams['select'], 0, 1);
+    }
     $api = \Civi\API\Request::create($savedSearch['api_entity'], 'get', $apiParams);
     $query = new \Civi\Api4\Query\Api4SelectQuery($api);
     $query->forceSelectId = FALSE;
     $query->getQuery()->having("$idField $excludeClause");
-    return $query->getSql();
+    $sql = $query->getSql();
+    // Place sql in a nested sub-query, otherwise HAVING is impossible on any field other than contact_id
+    return "SELECT $addSelect, `$idField` AS contact_id FROM ($sql) api_query";
   }
 
   /**
index fa5de2b9643defb793ab88509ec44e33770f81a7..50634d22bc8d3de08eb44da2143df2551c2bc82e 100644 (file)
@@ -109,6 +109,8 @@ class CRM_Utils_API_HTMLInputCoder extends CRM_Utils_API_AbstractFieldCoder {
         'header',
         // https://lab.civicrm.org/dev/core/issues/1286
         'footer',
+        // SavedSearch entity
+        'api_params',
       ];
       $custom = CRM_Core_DAO::executeQuery('SELECT id FROM civicrm_custom_field WHERE html_type = "RichTextEditor"');
       while ($custom->fetch()) {
index ece503cf2879d9c57eb7216db3d7e14e7dd78495..47837b2c8c10bfee694d3ebc62537bb9a8564a3d 100644 (file)
@@ -45,8 +45,7 @@
     }
 
     // Pick the first applicable column for contact id
-    model.api_params.select[0] = _.intersection(model.api_params.select, _.pluck($scope.columns, 'id'))[0] || $scope.columns[0].name;
-    model.api_params.select.length = 1;
+    model.api_params.select.unshift(_.intersection(model.api_params.select, _.pluck($scope.columns, 'id'))[0] || $scope.columns[0].id);
 
     if (!CRM.checkPerm('administer reserved groups')) {
       $scope.groupEntityRefParams.api.params.is_reserved = 0;
@@ -76,6 +75,7 @@
       group.visibility = model.visibility;
       group.group_type = model.group_type;
       group.saved_search_id = '$id';
+      model.api_params.select = _.unique(model.api_params.select);
       var savedSearch = {
         api_entity: model.api_entity,
         api_params: model.api_params
index 33ed7f3ca97b04a700f9beb481841577786b4066..cda1af0d57d51da7a3a658b0e7da63e21bf906d4 100644 (file)
           api_params: _.cloneDeep(angular.extend({}, ctrl.params, {version: 4}))
         };
         delete model.api_params.orderBy;
-        if (ctrl.load && ctrl.load.api_params) {
-          model.api_params.select = ctrl.load.api_params.select;
+        if (ctrl.load && ctrl.load.api_params && ctrl.load.api_params.select && ctrl.load.api_params.select[0]) {
+          model.api_params.select.unshift(ctrl.load.api_params.select[0]);
         }
         var options = CRM.utils.adjustDialogDefaults({
           autoOpen: false,
index a2ef0560473451d3b00c8ab3cc9729df92860dd2..3589ff2234015433e14eeb6f704f63a070467c4e 100644 (file)
@@ -1,6 +1,5 @@
 <form id="bootstrap-theme">
   <div ng-controller="SaveSmartGroup">
-    <div crm-ui-debug="model"></div>
     <input class="form-control" id="api-save-search-select-group" ng-model="model.id" crm-entityref="groupEntityRefParams" >
     <label ng-show="!model.id">{{:: ts('Or') }}</label>
     <input class="form-control" placeholder="{{:: ts('Create new group') }}" ng-model="model.title" ng-show="!model.id">
index 8b0b7ee4ed262061e4dd12fba926ef989a6d835a..859e89f2d41c4a33591969582a4a810b6f3cdca0 100644 (file)
@@ -83,4 +83,36 @@ class SavedSearchTest extends UnitTestCase {
     $this->assertArrayNotHasKey($out['id'], $ins['values']);
   }
 
+  public function testSmartGroupWithHaving() {
+    $in = Contact::create(FALSE)->addValue('first_name', 'yes')->addValue('last_name', 'siree')->execute()->first();
+    $in2 = Contact::create(FALSE)->addValue('first_name', 'yessir')->addValue('last_name', 'ee')->execute()->first();
+    $out = Contact::create(FALSE)->addValue('first_name', 'yess')->execute()->first();
+
+    $savedSearch = civicrm_api4('SavedSearch', 'create', [
+      'values' => [
+        'api_entity' => 'Contact',
+        'api_params' => [
+          'version' => 4,
+          'select' => ['id', 'CONCAT(first_name, last_name) AS whole_name'],
+          'where' => [
+            ['id', '>=', $in['id']],
+          ],
+          'having' => [
+            ['whole_name', '=', 'yessiree'],
+          ],
+        ],
+      ],
+      'chain' => [
+        'group' => ['Group', 'create', ['values' => ['title' => 'Having Test', 'saved_search_id' => '$id']], 0],
+      ],
+    ])->first();
+
+    // Oops we don't have an api4 syntax yet for selecting contacts in a group.
+    $ins = civicrm_api3('Contact', 'get', ['group' => $savedSearch['group']['name'], 'options' => ['limit' => 0]]);
+    $this->assertCount(2, $ins['values']);
+    $this->assertArrayHasKey($in['id'], $ins['values']);
+    $this->assertArrayHasKey($in2['id'], $ins['values']);
+    $this->assertArrayNotHasKey($out['id'], $ins['values']);
+  }
+
 }