From 4e97c26888a142cb3f48e236539209e61286db1a Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Sun, 1 Mar 2020 20:57:25 -0500 Subject: [PATCH] APIv4-based smart groups --- CRM/Contact/BAO/GroupContactCache.php | 48 ++++++++++++---- CRM/Contact/BAO/SavedSearch.php | 10 +++- Civi/Api4/Query/Api4SelectQuery.php | 19 +++++-- ang/api4Explorer.ang.php | 2 +- ang/api4Explorer/Explorer.html | 1 + ang/api4Explorer/Explorer.js | 52 ++++++++++++++++- ang/api4Explorer/SaveSearch.html | 14 +++++ .../phpunit/api/v4/Entity/SavedSearchTest.php | 57 +++++++++++++++++++ 8 files changed, 186 insertions(+), 17 deletions(-) create mode 100644 ang/api4Explorer/SaveSearch.html create mode 100644 tests/phpunit/api/v4/Entity/SavedSearchTest.php diff --git a/CRM/Contact/BAO/GroupContactCache.php b/CRM/Contact/BAO/GroupContactCache.php index d2247f153c..3234710a6b 100644 --- a/CRM/Contact/BAO/GroupContactCache.php +++ b/CRM/Contact/BAO/GroupContactCache.php @@ -462,20 +462,25 @@ WHERE id IN ( $groupIDs ) if ($savedSearchID) { $ssParams = CRM_Contact_BAO_SavedSearch::getSearchParams($savedSearchID); - // rectify params to what proximity search expects if there is a value for prox_distance - // CRM-7021 - if (!empty($ssParams)) { - CRM_Contact_BAO_ProximityQuery::fixInputParams($ssParams); - } - - if (isset($ssParams['customSearchID'])) { - $sql = self::getCustomSearchSQL($savedSearchID, $ssParams); + if (!empty($ssParams['api_entity'])) { + $mainCol = 'a'; + $sql = self::getApiSQL($savedSearchID, $ssParams); } else { - $sql = self::getQueryObjectSQL($savedSearchID, $ssParams); + $mainCol = 'contact_a'; + // CRM-7021 rectify params to what proximity search expects if there is a value for prox_distance + if (!empty($ssParams)) { + CRM_Contact_BAO_ProximityQuery::fixInputParams($ssParams); + } + if (isset($ssParams['customSearchID'])) { + $sql = self::getCustomSearchSQL($savedSearchID, $ssParams); + } + else { + $sql = self::getQueryObjectSQL($savedSearchID, $ssParams); + } } $groupID = CRM_Utils_Type::escape($groupID, 'Integer'); - $sql['from'] .= " AND contact_a.id NOT IN ( + $sql['from'] .= " AND $mainCol.id NOT IN ( SELECT contact_id FROM civicrm_group_contact WHERE civicrm_group_contact.status = 'Removed' AND civicrm_group_contact.group_id = $groupID ) "; @@ -712,6 +717,29 @@ ORDER BY gc.contact_id, g.children ]); } + /** + * @param $savedSearchID + * @param array $savedSearch + * @return array + * @throws API_Exception + * @throws \Civi\API\Exception\NotImplementedException + * @throws CRM_Core_Exception + */ + protected static function getApiSQL($savedSearchID, array $savedSearch): array { + $api = \Civi\API\Request::create($savedSearch['api_entity'], 'get', $savedSearch['api_params']); + $query = new \Civi\Api4\Query\Api4SelectQuery($api->getEntityName(), FALSE, $api->entityFields()); + $query->select = ['id']; + $query->where = $api->getWhere(); + $query->orderBy = $api->getOrderBy(); + $query->limit = $api->getLimit(); + $query->offset = $api->getOffset(); + $sql = $query->getSql(); + return [ + 'select' => substr($sql, 0, strpos($sql, 'FROM')), + 'from' => substr($sql, strpos($sql, 'FROM')), + ]; + } + /** * Get sql from a custom search. * diff --git a/CRM/Contact/BAO/SavedSearch.php b/CRM/Contact/BAO/SavedSearch.php index 6b64bcc711..f5753039ac 100644 --- a/CRM/Contact/BAO/SavedSearch.php +++ b/CRM/Contact/BAO/SavedSearch.php @@ -208,9 +208,17 @@ class CRM_Contact_BAO_SavedSearch extends CRM_Contact_DAO_SavedSearch { * @throws \CiviCRM_API3_Exception */ public static function getSearchParams($id) { + $savedSearch = \Civi\Api4\SavedSearch::get() + ->setCheckPermissions(FALSE) + ->addWhere('id', '=', $id) + ->execute() + ->first(); + if ($savedSearch['api_entity']) { + return $savedSearch; + } $fv = self::getFormValues($id); //check if the saved search has mapping id - if (CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_SavedSearch', $id, 'mapping_id')) { + if ($savedSearch['mapping_id']) { return CRM_Core_BAO_Mapping::formattedFields($fv); } elseif (!empty($fv['customSearchID'])) { diff --git a/Civi/Api4/Query/Api4SelectQuery.php b/Civi/Api4/Query/Api4SelectQuery.php index 4181543bbc..1291726b87 100644 --- a/Civi/Api4/Query/Api4SelectQuery.php +++ b/Civi/Api4/Query/Api4SelectQuery.php @@ -84,11 +84,14 @@ class Api4SelectQuery extends SelectQuery { } /** - * Why walk when you can + * Builds final sql statement after all params are set. * - * @return array|int + * @return string + * @throws \API_Exception + * @throws \CRM_Core_Exception + * @throws \Civi\API\Exception\UnauthorizedException */ - public function run() { + public function getSql() { $this->addJoins(); $this->buildSelectFields(); $this->buildWhereClause(); @@ -109,9 +112,17 @@ class Api4SelectQuery extends SelectQuery { if (!empty($this->limit) || !empty($this->offset)) { $this->query->limit($this->limit, $this->offset); } + return $this->query->toSQL(); + } + /** + * Why walk when you can + * + * @return array|int + */ + public function run() { $results = []; - $sql = $this->query->toSQL(); + $sql = $this->getSql(); if (is_array($this->debugOutput)) { $this->debugOutput['sql'][] = $sql; } diff --git a/ang/api4Explorer.ang.php b/ang/api4Explorer.ang.php index 9b974d53bf..816856b86d 100644 --- a/ang/api4Explorer.ang.php +++ b/ang/api4Explorer.ang.php @@ -13,5 +13,5 @@ return [ 'ang/api4Explorer', ], 'basePages' => [], - 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'crmRouteBinder', 'ui.sortable', 'api4', 'ngSanitize'], + 'requires' => ['crmUi', 'crmUtil', 'ngRoute', 'crmRouteBinder', 'ui.sortable', 'api4', 'ngSanitize', 'dialogService'], ]; diff --git a/ang/api4Explorer/Explorer.html b/ang/api4Explorer/Explorer.html index 5c9b3f4340..c98efef6a9 100644 --- a/ang/api4Explorer/Explorer.html +++ b/ang/api4Explorer/Explorer.html @@ -26,6 +26,7 @@ +
diff --git a/ang/api4Explorer/Explorer.js b/ang/api4Explorer/Explorer.js index ecf2d45973..45e9eb9643 100644 --- a/ang/api4Explorer/Explorer.js +++ b/ang/api4Explorer/Explorer.js @@ -20,7 +20,7 @@ }); }); - angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4) { + angular.module('api4Explorer').controller('Api4Explorer', function($scope, $routeParams, $location, $timeout, $http, crmUiHelp, crmApi4, dialogService) { var ts = $scope.ts = CRM.ts(); $scope.entities = entities; $scope.actions = actions; @@ -685,6 +685,56 @@ $scope.$watch('index', writeCode); writeCode(); + $scope.save = function() { + var model = { + title: '', + id: null, + entity: $scope.entity, + params: JSON.parse(angular.toJson($scope.params)) + }; + model.params.version = 4; + delete model.params.select; + delete model.params.chain; + delete model.params.debug; + delete model.params.limit; + delete model.params.checkPermissions; + var options = CRM.utils.adjustDialogDefaults({ + width: '500px', + autoOpen: false, + title: ts('Save smart group') + }); + dialogService.open('saveSearchDialog', '~/api4Explorer/SaveSearch.html', model, options); + }; + }); + + angular.module('api4Explorer').controller('SaveSearchCtrl', function($scope, crmApi4, dialogService) { + var ts = $scope.ts = CRM.ts(), + model = $scope.model; + $scope.$watch('model.id', function(id) { + if (id) { + model.description = $('#api-save-search-select-group').select2('data').extra.description; + } + }); + $scope.cancel = function() { + dialogService.cancel('saveSearchDialog'); + }; + $scope.save = function() { + $('.ui-dialog:visible').block(); + var group = model.id ? {id: model.id} : {title: model.title}; + group.description = model.description; + group.saved_search_id = '$id'; + var savedSearch = { + api_entity: model.entity, + api_params: model.params + }; + if (group.id) { + savedSearch.id = $('#api-save-search-select-group').select2('data').extra.saved_search_id; + } + crmApi4('SavedSearch', 'save', {records: [savedSearch], chain: {group: ['Group', 'save', {'records': [group]}]}}) + .then(function(result) { + dialogService.close('saveSearchDialog', result[0]); + }); + }; }); angular.module('api4Explorer').directive('crmApi4WhereClause', function($timeout) { diff --git a/ang/api4Explorer/SaveSearch.html b/ang/api4Explorer/SaveSearch.html new file mode 100644 index 0000000000..2935d325fe --- /dev/null +++ b/ang/api4Explorer/SaveSearch.html @@ -0,0 +1,14 @@ +
+
+ + + + + +
+
+ + +
+
+
diff --git a/tests/phpunit/api/v4/Entity/SavedSearchTest.php b/tests/phpunit/api/v4/Entity/SavedSearchTest.php new file mode 100644 index 0000000000..41bd7efd14 --- /dev/null +++ b/tests/phpunit/api/v4/Entity/SavedSearchTest.php @@ -0,0 +1,57 @@ +setCheckPermissions(FALSE)->addValue('first_name', 'yes')->addValue('do_not_phone', TRUE)->execute()->first(); + $out = Contact::create()->setCheckPermissions(FALSE)->addValue('first_name', 'no')->addValue('do_not_phone', FALSE)->execute()->first(); + + $savedSearch = civicrm_api4('SavedSearch', 'create', [ + 'values' => [ + 'api_entity' => 'Contact', + 'api_params' => [ + 'version' => 4, + 'where' => [ + ['do_not_phone', '=', TRUE], + ], + ], + ], + 'chain' => [ + 'group' => ['Group', 'create', ['values' => ['title' => 'Hello 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->assertArrayHasKey($in['id'], $ins['values']); + $this->assertArrayNotHasKey($out['id'], $ins['values']); + } + +} -- 2.25.1