CRM-19448 - Enable api joins across entity_table/entity_id fields
authorColeman Watts <coleman@civicrm.org>
Mon, 10 Oct 2016 15:27:02 +0000 (11:27 -0400)
committerColeman Watts <coleman@civicrm.org>
Wed, 12 Oct 2016 15:19:25 +0000 (11:19 -0400)
CRM/Core/BAO/EntityTag.php
CRM/Core/BAO/Note.php
CRM/Core/BAO/UFJoin.php
CRM/Core/PseudoConstant.php
CRM/Mailing/BAO/Mailing.php
Civi/API/SelectQuery.php
templates/CRM/Admin/Page/APIExplorer.js
templates/CRM/Admin/Page/APIExplorer.tpl
tests/phpunit/CiviTest/CiviUnitTestCase.php
tests/phpunit/api/v3/EntityTagTest.php
tests/phpunit/api/v3/NoteTest.php

index eec0888a27cae2bb0078166c2fbd41de0cdd78f3..dc14f3efad91d42af57a4d60bb91f25308c50998 100644 (file)
@@ -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;
   }
 
index a5646795eb8eb651696fb20162263e8cacb20f43..0a4d6a22cb74fced2fca3698fdd27dde95b893d5 100644 (file)
@@ -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);
   }
 
 }
index 7b7c7d76d5f8f5f73664ee55957f58100bb21450..d6a9e71b3bce659ff67681656e7abb776cb45dbf 100644 (file)
@@ -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);
   }
 
 }
index 9638078a6da2a801fc50a63df72d52dc820d508e..51a4b56eba0a94b114e09072af1fa14e3d71358a 100644 (file)
@@ -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;
index 25cd5bb485c523371e5aeee1bc1ba960fd458cca..a04124a4760c1033596214b393b4435338678670 100644 (file)
@@ -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);
   }
 
   /**
index fc5d22cad5b5ed3ce4d191ffdc36300137a55c06..357dc92ceb1a9737a09cf122a3f6069f0c6f8a0a 100644 (file)
@@ -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
    *
index 5ce9482aadb85a9ae91e9fa1b35b0ea3b707ccaf..1360611a49966f4d76341aee008c1482916055fa 100644 (file)
    * @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]];
+      }
+    }
   }
 
   /**
     });
   }
 
+  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
    *
     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
       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]
             };
               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}));
         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')
index ae17f57af3e2f48df0ac725a71f35ea7022248d8..bdc3cd2c6e6c8d00ad760169df128c349b14c3a5 100644 (file)
   #api-join li.join-enabled > i {
     opacity: 1;
   }
+  #api-join li.join-not-available {
+    font-style: italic;
+  }
   #api-generated-wraper,
   #api-result {
     overflow: auto;
   {literal}
   <ul class="fa-ul">
     <% _.forEach(joins, function(join, name) { %>
-      <li <% if(join.checked) { %>class="join-enabled"<% } %>>
+      <li <% if(join.checked) { %>class="join-enabled"<% } if(join.disabled) { %>class="join-not-available"<% }%>>
         <i class="fa-li crm-i fa-reply fa-rotate-180"></i>
         <label for="select-join-<%= name %>" class="api-checkbox-label">
-          <input type="checkbox" id="select-join-<%= name %>" value="<%= name %>" data-entity="<%= join.entity %>" <% if(join.checked) { %>checked<% } %>/>
+          <input type="checkbox" id="select-join-<%= name %>" value="<%= name %>" data-entity="<%= join.entity %>" <% if(join.checked) { %>checked<% } if(join.disabled) { %>disabled<% } %>/>
           <%- join.title %>
         </label>
       </li>
index 21065f13ee94b8731051e83edac1cf26be9ec86b..86cda4b725ef258d3020559e173ace78e1381067 100644 (file)
@@ -2388,17 +2388,6 @@ class CiviUnitTestCase extends PHPUnit_Extensions_Database_TestCase {
     }
   }
 
-  /**
-   * Delete note.
-   *
-   * @param array $params
-   *
-   * @return array|int
-   */
-  public function noteDelete($params) {
-    return $this->callAPISuccess('Note', 'delete', $params);
-  }
-
   /**
    * Create custom field with Option Values.
    *
index 57347df931ea3b99ed81e7c8bfc8384a8f11cf79..a0afa3eb9f50f1fb003e9b6170e8eb092383816f 100644 (file)
@@ -64,7 +64,7 @@ class api_v3_EntityTagTest extends CiviUnitTestCase {
     $this->useTransaction(TRUE);
 
     $this->_individualID = $this->individualCreate();
-    $this->_tag = $this->tagCreate();
+    $this->_tag = $this->tagCreate(array('name' => 'EntityTagTest'));
     $this->_tagID = $this->_tag['id'];
     $this->_householdID = $this->houseHoldCreate();
     $this->_organizationID = $this->organizationCreate();
@@ -300,4 +300,30 @@ class api_v3_EntityTagTest extends CiviUnitTestCase {
     $this->assertEquals($result['not_removed'], 1);
   }
 
+  public function testEntityTagJoin() {
+    $org = $this->callAPISuccess('Contact', 'create', array(
+      'contact_type' => 'Organization',
+      'organization_name' => 'Org123',
+      'api.EntityTag.create' => array(
+        'tag_id' => $this->_tagID,
+      ),
+    ));
+    // Fetch contact info via join
+    $result = $this->callAPISuccessGetSingle('EntityTag', array(
+      'return' => array("entity_id.organization_name", "tag_id.name"),
+      'entity_id' => $org['id'],
+      'entity_table' => "civicrm_contact",
+    ));
+    $this->assertEquals('Org123', $result['entity_id.organization_name']);
+    $this->assertEquals('EntityTagTest', $result['tag_id.name']);
+    // This should return no results by restricting contact_type
+    $result = $this->callAPISuccess('EntityTag', 'get', array(
+      'return' => array("entity_id.organization_name"),
+      'entity_id' => $org['id'],
+      'entity_table' => "civicrm_contact",
+      'entity_id.contact_type' => "Individual",
+    ));
+    $this->assertEquals(0, $result['count']);
+  }
+
 }
index e0e6fd63b650bf7bb6af9aa89e6e5d8f5dc8321b..e799bee3906ba8fc2489d02533e3fbb38e63b41e 100644 (file)
@@ -137,10 +137,6 @@ class api_v3_NoteTest extends CiviUnitTestCase {
     $this->assertEquals(date('Y-m-d', strtotime($this->_params['modified_date'])), date('Y-m-d', strtotime($result['values'][$result['id']]['modified_date'])));
 
     $this->assertArrayHasKey('id', $result);
-    $note = array(
-      'id' => $result['id'],
-    );
-    $this->noteDelete($note);
   }
 
   public function testCreateWithApostropheInString() {
@@ -158,12 +154,6 @@ class api_v3_NoteTest extends CiviUnitTestCase {
     $this->assertEquals($result['values'][0]['note'], "Hello!!! ' testing Note");
     $this->assertEquals($result['values'][0]['subject'], "With a '");
     $this->assertArrayHasKey('id', $result);
-
-    //CleanUP
-    $note = array(
-      'id' => $result['id'],
-    );
-    $this->noteDelete($note);
   }
 
   /**
@@ -174,9 +164,6 @@ class api_v3_NoteTest extends CiviUnitTestCase {
     $apiResult = $this->callAPISuccess('note', 'create', $this->_params);
     $this->assertAPISuccess($apiResult);
     $this->assertEquals(date('Y-m-d'), date('Y-m-d', strtotime($apiResult['values'][$apiResult['id']]['modified_date'])));
-    $this->noteDelete(array(
-      'id' => $apiResult['id'],
-    ));
   }
 
   /**
@@ -259,6 +246,32 @@ class api_v3_NoteTest extends CiviUnitTestCase {
     $this->callAPIAndDocument('note', 'delete', $params, __FUNCTION__, __FILE__);
   }
 
+  public function testNoteJoin() {
+    $org = $this->callAPISuccess('Contact', 'create', array(
+      'contact_type' => 'Organization',
+      'organization_name' => 'Org123',
+      'api.Note.create' => array(
+        'note' => 'Hello join',
+      ),
+    ));
+    // Fetch contact info via join
+    $result = $this->callAPISuccessGetSingle('Note', array(
+      'return' => array("entity_id.organization_name", "note"),
+      'entity_id' => $org['id'],
+      'entity_table' => "civicrm_contact",
+    ));
+    $this->assertEquals('Org123', $result['entity_id.organization_name']);
+    $this->assertEquals('Hello join', $result['note']);
+    // This should return no results by restricting contact_type
+    $result = $this->callAPISuccess('Note', 'get', array(
+      'return' => array("entity_id.organization_name"),
+      'entity_id' => $org['id'],
+      'entity_table' => "civicrm_contact",
+      'entity_id.contact_type' => "Individual",
+    ));
+    $this->assertEquals(0, $result['count']);
+  }
+
 }
 
 /**