From 466fce54355aee7e1f74dd070747eb055303b677 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Mon, 10 Oct 2016 11:27:02 -0400 Subject: [PATCH] CRM-19448 - Enable api joins across entity_table/entity_id fields --- CRM/Core/BAO/EntityTag.php | 10 +++ CRM/Core/BAO/Note.php | 12 ++-- CRM/Core/BAO/UFJoin.php | 10 ++- CRM/Core/PseudoConstant.php | 2 +- CRM/Mailing/BAO/Mailing.php | 10 ++- Civi/API/SelectQuery.php | 20 +++++- templates/CRM/Admin/Page/APIExplorer.js | 73 ++++++++++++++------- templates/CRM/Admin/Page/APIExplorer.tpl | 7 +- tests/phpunit/CiviTest/CiviUnitTestCase.php | 11 ---- tests/phpunit/api/v3/EntityTagTest.php | 28 +++++++- tests/phpunit/api/v3/NoteTest.php | 39 +++++++---- 11 files changed, 152 insertions(+), 70 deletions(-) diff --git a/CRM/Core/BAO/EntityTag.php b/CRM/Core/BAO/EntityTag.php index eec0888a27..dc14f3efad 100644 --- a/CRM/Core/BAO/EntityTag.php +++ b/CRM/Core/BAO/EntityTag.php @@ -464,6 +464,16 @@ class CRM_Core_BAO_EntityTag extends CRM_Core_DAO_EntityTag { $options = CRM_Core_PseudoConstant::get(__CLASS__, $fieldName, $params, $context); + // Special formatting for validate/match context + if ($fieldName == 'entity_table' && in_array($context, array('validate', 'match'))) { + $options = array(); + foreach (self::buildOptions($fieldName) as $tableName => $label) { + $bao = CRM_Core_DAO_AllCoreTables::getClassForTable($tableName); + $apiName = CRM_Core_DAO_AllCoreTables::getBriefName($bao); + $options[$tableName] = $apiName; + } + } + return $options; } diff --git a/CRM/Core/BAO/Note.php b/CRM/Core/BAO/Note.php index a5646795eb..0a4d6a22cb 100644 --- a/CRM/Core/BAO/Note.php +++ b/CRM/Core/BAO/Note.php @@ -585,14 +585,12 @@ WHERE participant.contact_id = %1 AND note.entity_table = 'civicrm_participant' * @return array */ public static function entityTables() { - $tables = array( - 'civicrm_relationship', - 'civicrm_contact', - 'civicrm_participant', - 'civicrm_contribution', + return array( + 'civicrm_relationship' => 'Relationship', + 'civicrm_contact' => 'Contact', + 'civicrm_participant' => 'Participant', + 'civicrm_contribution' => 'Contribution', ); - // Identical keys & values - return array_combine($tables, $tables); } } diff --git a/CRM/Core/BAO/UFJoin.php b/CRM/Core/BAO/UFJoin.php index 7b7c7d76d5..d6a9e71b3b 100644 --- a/CRM/Core/BAO/UFJoin.php +++ b/CRM/Core/BAO/UFJoin.php @@ -186,13 +186,11 @@ class CRM_Core_BAO_UFJoin extends CRM_Core_DAO_UFJoin { * @return array */ public static function entityTables() { - $tables = array( - 'civicrm_event', - 'civicrm_contribution_page', - 'civicrm_survey', + return array( + 'civicrm_event' => 'Event', + 'civicrm_contribution_page' => 'ContributionPage', + 'civicrm_survey' => 'Survey', ); - // Identical keys & values - return array_combine($tables, $tables); } } diff --git a/CRM/Core/PseudoConstant.php b/CRM/Core/PseudoConstant.php index 9638078a6d..51a4b56eba 100644 --- a/CRM/Core/PseudoConstant.php +++ b/CRM/Core/PseudoConstant.php @@ -246,7 +246,7 @@ class CRM_Core_PseudoConstant { // if callback is specified.. if (!empty($pseudoconstant['callback'])) { - $fieldOptions = call_user_func(Civi\Core\Resolver::singleton()->get($pseudoconstant['callback'])); + $fieldOptions = call_user_func(Civi\Core\Resolver::singleton()->get($pseudoconstant['callback']), $context); //CRM-18223: Allow additions to field options via hook. CRM_Utils_Hook::fieldOptions($entity, $fieldName, $fieldOptions, $params); return $fieldOptions; diff --git a/CRM/Mailing/BAO/Mailing.php b/CRM/Mailing/BAO/Mailing.php index 25cd5bb485..a04124a476 100644 --- a/CRM/Mailing/BAO/Mailing.php +++ b/CRM/Mailing/BAO/Mailing.php @@ -3171,13 +3171,11 @@ AND m.id = %1 * Whitelist of possible values for the entity_table field * @return array */ - public static function mailingGroupEntityTables() { - $tables = array( - CRM_Contact_BAO_Group::getTableName(), - CRM_Mailing_BAO_Mailing::getTableName(), + public static function mailingGroupEntityTables($context = NULL) { + return array( + CRM_Contact_BAO_Group::getTableName() => 'Group', + CRM_Mailing_BAO_Mailing::getTableName() => 'Mailing', ); - // Identical keys & values - return array_combine($tables, $tables); } /** diff --git a/Civi/API/SelectQuery.php b/Civi/API/SelectQuery.php index fc5d22cad5..357dc92ceb 100644 --- a/Civi/API/SelectQuery.php +++ b/Civi/API/SelectQuery.php @@ -215,11 +215,12 @@ abstract class SelectQuery { if ($depth > self::MAX_JOINS) { throw new UnauthorizedException("Maximum number of joins exceeded in parameter $fkFieldName"); } + $subStack = array_slice($stack, 0, $depth); + $this->getJoinInfo($fkField, $subStack); if (!isset($fkField['FKApiName']) || !isset($fkField['FKClassName'])) { // Join doesn't exist - might be another param with a dot in it for some reason, we'll just ignore it. return NULL; } - $subStack = array_slice($stack, 0, $depth); // Ensure we have permission to access the other api if (!$this->checkPermissionToJoin($fkField['FKApiName'], $subStack)) { throw new UnauthorizedException("Authorization failed to join onto {$fkField['FKApiName']} api in parameter $fkFieldName"); @@ -257,6 +258,23 @@ abstract class SelectQuery { return array($tableAlias, $fieldName); } + /** + * Get join info for dynamically-joined fields (e.g. "entity_id") + * + * @param $fkField + * @param $stack + */ + protected function getJoinInfo(&$fkField, $stack) { + if ($fkField['name'] == 'entity_id') { + $entityTableParam = substr(implode('.', $stack), 0, -2) . 'table'; + $entityTable = \CRM_Utils_Array::value($entityTableParam, $this->where); + if ($entityTable && is_string($entityTable) && \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable)) { + $fkField['FKClassName'] = \CRM_Core_DAO_AllCoreTables::getClassForTable($entityTable); + $fkField['FKApiName'] = \CRM_Core_DAO_AllCoreTables::getBriefName($fkField['FKClassName']); + } + } + } + /** * Joins onto a custom field * diff --git a/templates/CRM/Admin/Page/APIExplorer.js b/templates/CRM/Admin/Page/APIExplorer.js index 5ce9482aad..1360611a49 100644 --- a/templates/CRM/Admin/Page/APIExplorer.js +++ b/templates/CRM/Admin/Page/APIExplorer.js @@ -98,24 +98,36 @@ * @returns {*} */ function getField(name) { - if (!name) { - return {}; - } - if (getFieldData[name]) { - return getFieldData[name]; - } - var ent = entity, - act = action, - prefix = ''; - _.each(name.split('.'), function(piece) { - if (joins[prefix]) { - ent = joins[prefix]; - act = 'get'; + var field = {}; + if (name && getFieldData[name]) { + field = _.cloneDeep(getFieldData[name]); + } else if (name) { + var ent = entity, + act = action, + prefix = ''; + _.each(name.split('.'), function(piece) { + if (joins[prefix]) { + ent = joins[prefix]; + act = 'get'; + } + name = piece; + prefix += (prefix.length ? '.' : '') + piece; + }); + if (getFieldsCache[ent+act].values[name]) { + field = _.cloneDeep(getFieldsCache[ent+act].values[name]); } - name = piece; - prefix += (prefix.length ? '.' : '') + piece; - }); - return getFieldsCache[ent+act].values[name] || {}; + } + addJoinInfo(field, name); + return field; + } + + function addJoinInfo(field, name) { + if (field.name === 'entity_id') { + var entityTableParam = name.slice(0, -2) + 'table'; + if (params[entityTableParam]) { + field.FKApiName = getField(entityTableParam).options[params[entityTableParam]]; + } + } } /** @@ -276,6 +288,15 @@ }); } + function changeFKEntity() { + var $row = $(this).closest('tr'), + name = $('input.api-param-name', $row).val(), + operator = $('.api-param-op', $row).val(); + if (name && name.slice(-12) === 'entity_table') { + $('input[value=' + name.slice(0, -5) + 'id]', '#api-join').prop('checked', false).change(); + } + } + /** * For "get" actions show the "return" options * @@ -395,7 +416,7 @@ if (operator !== '=') { return false; } - return true; + return fieldName !== 'entity_table'; /* * Attempt to resolve the ambiguity of the = operator using metadata * commented out because there is not enough metadata in the api at this time @@ -832,11 +853,12 @@ var joinable = {}; (function recurse(fields, joinable, prefix, depth, entities) { _.each(fields, function(field) { + var name = prefix + field.name; + addJoinInfo(field, name); var entity = field.FKApiName; - if (entity && field.FKClassName) { - var name = prefix + field.name; + if (entity) { joinable[name] = { - title: field.title, + title: field.title + ' (' + field.FKApiName + ')', entity: entity, checked: !!joins[name] }; @@ -845,9 +867,15 @@ joinable[name].children = {}; recurse(getFieldsCache[entity+'get'].values, joinable[name].children, name + '.', depth+1, entities.concat(entity)); } + } else if (field.name == 'entity_id' && fields.entity_table && fields.entity_table.options) { + joinable[name] = { + title: field.title + ' (' + ts('First select %1', {1: fields.entity_table.title}) + ')', + entity: '', + disabled: true + }; } }); - })(getFieldData, joinable, '', 1, [entity]); + })(_.cloneDeep(getFieldData), joinable, '', 1, [entity]); if (!_.isEmpty(joinable)) { // Send joinTpl as a param so it can recursively call itself to render children $('#api-join').show().children('div').html(joinTpl({joins: joinable, tpl: joinTpl})); @@ -918,6 +946,7 @@ checkBookKeepingEntity(entity, action); }) .on('change keyup', 'input.api-input, #api-params select', buildParams) + .on('change', '.api-param-name, .api-param-value, .api-param-op', changeFKEntity) .on('submit', submit); $('#api-params') diff --git a/templates/CRM/Admin/Page/APIExplorer.tpl b/templates/CRM/Admin/Page/APIExplorer.tpl index ae17f57af3..bdc3cd2c6e 100644 --- a/templates/CRM/Admin/Page/APIExplorer.tpl +++ b/templates/CRM/Admin/Page/APIExplorer.tpl @@ -107,6 +107,9 @@ #api-join li.join-enabled > i { opacity: 1; } + #api-join li.join-not-available { + font-style: italic; + } #api-generated-wraper, #api-result { overflow: auto; @@ -389,10 +392,10 @@ {literal}