Browse Source

Merge pull request #470 from tripal/467-tv3-entity_relationships

Support for Views Tripal Entity Relationship
Stephen Ficklin 6 years ago
parent
commit
841bbe1f0a

+ 160 - 32
tripal/includes/TripalFieldQuery.inc

@@ -11,10 +11,68 @@ class TripalFieldQuery extends EntityFieldQuery {
    */
   protected $includes = array();
 
+  /**
+   * These member variables keep track of join relationships with the
+   * tripal_entity table. This is useful for non Tripal Storage API fields that
+   * want to filter based on on other Drupal tables. The most important 
+   * example for this would be Drupal Views.  These variables are only meant
+   * to be used by the tripal_field_storage_query() function as that is the 
+   * only storage system that should be doing quries on the tripal_entity
+   * table itself.
+   */
+  public $relationships = [];
+  public $relationshipConditions = [];
+
+  /**
+   * Adds a relationsihp via a table join to the tripal_entity table
+   * 
+   * This is specific for Drupal schema tables and is useful for Views 
+   * integration when non Tripal Storage API fields are attached to an entity.
+   * 
+   * @param $table
+   *   The name of the table to join.
+   * @param $alias
+   *   The alias for the table.
+   * @param $field
+   *   The field to join on.
+   */ 
+  public function addRelationship($table, $alias, $field) {    
+    $this->relationships[$alias] = [
+      'table' => $table,
+      'field' => $field,
+    ];
+  }
+  /**
+   * Adds a where statement to a relationship.
+   * 
+   * The relationship is added by the $table
+   */
+  public function relationshipCondition($table, $field, $value, $op) {
+
+    $table_alias = '';
+    // Get the alias for this table.
+    foreach ($this->relationships as $alias => $details) {
+      if ($details['table'] == $table) {
+        $table_alias = $alias;
+      }
+    }
+    
+    if ($table_alias) {
+      $this->relationshipConditions[$table_alias] = [
+        'field' => $field,
+        'value' => $value,
+        'op' => $op,      
+      ];
+    }
+  }
+
   /**
    * Overrides the EntityFieldQuery::execute() function.
    */
   public function execute() {
+    // Initialize the results array.
+    $results = array('first_results' => TRUE);
+    
     // Give a chance for other modules to alter the query.
     drupal_alter('entity_query', $this);
     $this->altered = TRUE;
@@ -32,49 +90,117 @@ class TripalFieldQuery extends EntityFieldQuery {
         $this->field_storage[$field['storage']['type']] = $field['storage']['module'];
       }
 
-      // Initialize the results array.
-      $results = array();
-
       // Iterate through the field storage types and call each one.
       foreach ($this->field_storage as $storage_type => $storage_module) {
-        // Execute the query using the correct callback.
         $callback = $this->queryStorageCallback($storage_module);
-
         $st_results = call_user_func($callback, $this);
-        // If this is the first storage type to be queries then save these
-        // as the current results list.
-        if (count($results) == 0) {
-          $results = $st_results;
-        }
-        // If other results already exist then we want to find the intersection
-        // of the two and only save those.
-        else {
-          $intersection = array(
-            'TripalEntity' => array(),
-          );
-          foreach ($st_results['TripalEntity'] as $entity_id => $stub) {
-            if (array_key_exists($entity_id, $results['TripalEntity'])) {
-              $intersection['TripalEntity'][$entity_id] = $stub;
-            }
-          }
-          $results = $intersection;
-        }
+        $results = $this->_intersectResults($results, $st_results);
       }
     }
-    // If there are no fields then default to the original
-    // EntityFieldQuery() functionality.
-    else {
-      $results = call_user_func($this->queryCallback(), $this);
+            
+    // If we have relationships, then handle those.
+    if (!empty($this->relationshipConditions)) {
+      $st_results = tripal_field_storage_query($this);
+      $results = $this->_intersectResults($results, $st_results);
+      
+      // Are there any other callbacks that need running other than
+      // propertyQuery as we would have run those with the relationships.
+      $callback = $this->queryCallback();
+      if ($callback and $callback[1] != 'propertyQuery' and 
+          $callback[1] != 'tripal_field_storage_query') {
+        $st_results = call_user_func($callback, $this);
+        $results = $this->_intersectResults($results, $st_results);
+      }
+    }
+    // There are no fields or relationships so just use the default 
+    // callback for the query.
+    else if (!$this->fields){
+      $callback = $this->queryCallback();
+      $st_results = call_user_func($callback, $this);
+      $results = $this->_intersectResults($results, $st_results);
     }
 
-    if ($results and $this->count) {
-      if (!is_numeric($results)) {
-        throw new Exception('Query callback function did not provide a numeric value: ' . $this->queryCallback());
+    // If this is a count query then it should return a numeric value.
+    if ($this->count) {
+      if (!$results or !is_array($results)) {
+        return 0;
       }
-      return $results;
+      return count($results['TripalEntity']);
+    }
+    return $results;
+  }
+  
+  /**
+   * Generates an intersection of results from different storage back-ends.
+   */
+  protected function _intersectResults($current_results, $new_results) {
+
+    // If we currently don't have any results then just allow all through.
+    // This is the first result set.
+    if (array_key_exists('first_results', $current_results)) {
+      return $new_results;
+    }
+
+    // set defaults to prevent warnings
+    if (empty($new_results)){
+    $new_results['TripalEntity'] = [];
+    }
+    if (empty($current_results)){
+      $current_results['TripalEntity'] = [];
+    }
+
+    // Iterate through all of the new results and only include those that
+    // exist in both the current and new.
+    $intersection = [];
+    foreach ($new_results['TripalEntity'] as $entity_id => $stub) {
+      if (array_key_exists($entity_id, $current_results['TripalEntity'])) {
+        $intersection[$entity_id] = $stub;
+      }
+    }
+    if (count($intersection) > 0) {
+      return ['TripalEntity' => $intersection];
+    }
+    else {
+      return [];
+    }
+  }
+  
+  /**
+   * Overides the EntityFieldQuery::queryCallback function.
+   */
+  public function queryCallback() {
+    
+    // Use the override from $this->executeCallback. It can be set either
+    // while building the query, or using hook_entity_query_alter().
+    if (function_exists($this->executeCallback)) {
+      return $this->executeCallback;
+    }
+    
+    // If there are no field conditions and sorts, and no execute callback
+    // then we default to querying entity tables in SQL.
+    if (empty($this->fields)) {
+      return array(
+        $this,
+        'propertyQuery',
+      );
+    }
+    
+    // If no override, find the storage engine to be used.
+    foreach ($this->fields as $field) {
+      if (!isset($storage)) {
+        $storage = $field['storage']['module'];
+      }
+      elseif ($storage != $field['storage']['module']) {
+        throw new EntityFieldQueryException(t("Can't handle more than one field storage engine"));
+      }
+    }
+    if ($storage) {
+      
+      // Use hook_field_storage_query() from the field storage.
+      return $storage . '_field_storage_query';
     }
     else {
-      return $results;
+      throw new EntityFieldQueryException(t("Field storage engine not found."));
     }
   }
 
@@ -94,11 +220,13 @@ class TripalFieldQuery extends EntityFieldQuery {
    *
    */
   protected function queryStorageCallback($storage) {
+    
     // Use the override from $this->executeCallback. It can be set either
     // while building the query, or using hook_entity_query_alter().
     if (function_exists($this->executeCallback)) {
       return $this->executeCallback;
     }
+    
     // If there are no field conditions and sorts, and no execute callback
     // then we default to querying entity tables in SQL.
     if (empty($this->fields)) {

+ 22 - 3
tripal/includes/tripal.field_storage.inc

@@ -14,7 +14,9 @@ function tripal_field_storage_info() {
       'label' => t('Tripal'),
       'description' => t('The NULL storage is a placeholder for field values
           that are not stored in any storage backend (e.g. entity types).'),
-      'settings' => array(),
+      'settings' => array(
+        'tripal_storage_api' => TRUE,
+      ),
     ),
   );
 }
@@ -84,6 +86,18 @@ function tripal_field_storage_query($query) {
     }
   }
 
+  if ($query->relationshipConditions) {
+    foreach ($query->relationshipConditions as $table_alias => $reldetails) {
+      $field = $reldetails['field'];
+      $value = $reldetails['value'];
+      $op = $reldetails['op'];        
+      $relationship = $query->relationships[$table_alias];
+      $table = $relationship['table'];
+      $select->join($table, $table_alias, 'TE.id = ' . $table_alias . '.' . $relationship['field']);
+      $select->condition($table_alias . '.' . $field, $value, $op);
+    }
+  }
+
   // Add in any filters to the query.
   foreach ($query->fieldConditions as $index => $condition) {
     $field = $condition['field'];
@@ -117,21 +131,26 @@ function tripal_field_storage_query($query) {
   }
   
   // Add a range of records to retrieve
-  if (isset($query->range)) {
+  if ($query->range) {
     $select->range($query->range['start'], $query->range['length']);
   }
   
   // Only include records that are deleted.  Tripal doesn't keep track of
   // records that are deleted that need purging separately so we can do nothing
   // with this.
-  if (isset($query->deleted)) {
+  if (property_exists($query, 'deleted') and $query->deleted) {
     // There won't ever be field data marked as deleted so just created a 
     // condition that always evaluates to false.
     $select->where('1=0');
   }
 
+//   dpm($query);
+//   dpm($select->__toString());
+//   dpm($select->getArguments());
+  
   // Perform the query and return the results.
   $entities = $select->execute();
+  
   $result = array(
     'TripalEntity' => array(),
   );

+ 13 - 3
tripal/includes/tripal.fields.inc

@@ -88,6 +88,15 @@ function tripal_field_widget_info_alter(&$info) {
 function tripal_field_views_data($field) {
   $data = array();
 
+  // Skip fields that aren't attached to TripalEntity entities.
+  if (!array_key_exists('TripalEntity', $field['bundles'])) {
+    return $data;
+  }
+
+  if (!array_key_exists('tripal_storage_api', $field['storage']['settings'])) {
+    return $data;
+  }
+
   $field_type = $field['type'];
   $field_types = tripal_get_field_types();
 
@@ -953,14 +962,15 @@ function tripal_field_is_empty($item, $field) {
     return TRUE;
   }
 
-  // If there is no value field then the field is empty.
-  if (!array_key_exists('value', $item)) {
+  // If the field is a tripal field storage API field and there 
+  // is no value field then the field is empty.
+  if (array_key_exists('tripal_storage_api', $field['storage']['settings']) and !array_key_exists('value', $item)) {
     return TRUE;
   }
 
   // If there is a value field but there's nothing in it, the the field is
   // empty.
-  if (array_key_exists('value', $item) and empty($item['value'])){
+  if (array_key_exists('value', $item) and empty($item['value'])) {
     return TRUE;
   }
 

+ 29 - 0
tripal/tripal.install

@@ -1180,3 +1180,32 @@ function tripal_update_7311() {
     throw new DrupalUpdateException('Could not perform update: '. $error);
   }
 }
+
+/**
+ * Adds a tripal_storage_api setting to all field storage details
+ */
+function tripal_update_7312() {
+  $transaction = db_transaction();
+  try {
+    $fields = db_select('field_config', 'fc')
+      ->fields('fc', ['id', 'storage_type', 'data'])
+      ->where("fc.storage_type = 'field_chado_storage' or fc.storage_type = 'tripal_no_storage'")
+      ->execute();
+    while ($field = $fields->fetchObject()) {
+      $data = unserialize($field->data); 
+      $data['storage']['settings']['tripal_storage_api'] = TRUE;
+      db_update('field_config')
+        ->fields([
+          'data' => serialize($data)
+        ])
+        ->condition('id', $field->id)
+        ->execute();
+    }
+  }
+  catch (\PDOException $e) {
+    $transaction->rollback();
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not perform update: '. $error);
+  }
+}
+

+ 64 - 37
tripal/tripal.views.inc

@@ -51,42 +51,64 @@ function tripal_views_data() {
  */
 function tripal_views_data_alter(&$data) {
 
-  $fields = field_info_fields();
-  $tripal_fields = tripal_get_field_types();
-
-  // Iterate through all of the fields.
-  foreach ($fields as $field) {
-
-    // Skip fields that aren't attached to TripalEntity entities.
-    if (!array_key_exists('TripalEntity', $field['bundles'])) {
-      continue;
-    }
-
-    // Iterate through the bundles to which this field is attached and
-    // if it is a TripalField field then we'll call the viewsData function.
-    $bundles = $field['bundles']['TripalEntity'];
-    $result = array();
-    foreach ($bundles as $bundle_name) {
-      $field_name = $field['field_name'];
-
-      // Skip fields that aren't setup for views with this bundle.
-      // Fields should be associated with the bundle's term identifier
-      // (i.e. [vocab]__[accession].
-      $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
-      $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
-      $bundle_term_id = $term->vocab->vocabulary . '__' . $term->accession;
-      if (!array_key_exists($bundle_term_id, $data)) {
-        continue;
-      }
-
-      // Skip fields implemented using a TripalField class. These fields
-      // should already have the proper handlers because the class sets
-      // them up automatically.
-      if (in_array($field_name, $tripal_fields)) {
-        continue;
-      }
-
-      $data[$bundle_term_id][$field_name]['sort']['handler'] = 'tripal_views_handler_sort';
+  // Iterate through all of the views data and find
+  // those that are associated with fields attached to 
+  // Tripal entities.  For known field types (e.g. Taxonomy) we
+  // can support those.
+  foreach ($data as $data_table => $definition) {
+    foreach ($definition as $data_column => $element) {
+      if (is_array($element) and array_key_exists('field', $element) and 
+          is_array($element['field']) and array_key_exists('field_name', $element['field'])) {
+        $field_name = $element['field']['field_name'];
+        $field = field_info_field($field_name);
+
+        // Skip fields that aren't attached to a TripalEntity content type.
+        if (!array_key_exists('TripalEntity', $field['bundles'])) {
+          continue;
+        }
+
+        // Skip fields that use the Tripal Storage API.
+        if (array_key_exists('tripal_storage_api', $field['storage']['settings'])) {
+          continue;
+        }
+
+        //  
+        // Now update views for integrating other data with our Tripal Entities.
+        //
+    
+        // Iterate through the bundles to which this field is attached and
+        // if it is a TripalField field then we'll call the viewsData function.
+        $bundles = $field['bundles']['TripalEntity'];
+        $result = array();
+        foreach ($bundles as $bundle_name) {
+
+          // Skip fields that aren't setup for views with this bundle.
+          // Fields should be associated with the bundle's term identifier
+          // (i.e. [vocab]__[accession].
+          $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
+          $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
+          $bundle_term_id = $term->vocab->vocabulary . '__' . $term->accession;
+          if (!array_key_exists($bundle_term_id, $data)) {
+            continue;
+          }
+
+          // Support the taxonomy_term_reference field when it's added to a 
+          // Tripal content type
+          if ($field['type'] == 'taxonomy_term_reference') { 
+            $data[$bundle_term_id][$field_name] = [
+              'title' => t('Tagged Categories'), 
+              'help' => t('Relates this Tripal content type to categories that have been assigned to it using Drupal\'s Taxonomy system.'),
+              'relationship' => array(
+                 'base' => $data_table,
+                 'base field' => 'entity_id',
+                 'relationship field' => 'entity_id',
+                 'handler' => 'views_handler_relationship',
+                 'label' => t('Tags'),
+              ),
+            ];
+          } 
+        }
+      } 
     }
   }
 }
@@ -104,6 +126,11 @@ function tripal_views_data_fields(&$data) {
       continue;
     }
 
+    // Fields that don't connect to the Tripal Storage API should be added differently
+    if (!array_key_exists('tripal_storage_api', $field['storage']['settings'])) {
+      continue;
+    }
+
     // TODO: do we really need this hook? Just substitute the code here
     // for what that hook does... it's only called in one plac.e
     // Call the hook_field_views_data() but only for the tripal module.
@@ -482,4 +509,4 @@ function tripal_views_data_jobs(&$data) {
       'handler' => 'views_handler_sort',
     ),
   );
-}
+}

+ 70 - 23
tripal/tripal_views_query.inc

@@ -87,7 +87,6 @@ class tripal_views_query extends views_plugin_query {
    *   here.
    */
   public function add_where($group, $field_name, $value = NULL, $operator = NULL) {
-
     if ($value) {
 
       $this->filters[] = array(
@@ -104,35 +103,65 @@ class tripal_views_query extends views_plugin_query {
         return;
       }
 
-      // For Tripal create fields the name of the field is an encoded
-      // string that contains the bundle term, field name and any
-      // sub elements. We need to extract them.
+      // For fields compatible with the Tripal storage API, the
+      // incoming $field_name is a combination of the entity term ID,
+      // followed by the field name and the the sub element string, with
+      // sub elements children separated by a comma.  For non Tripal 
+      // storage API the $field_name is a combination of the table name
+      // followed by the table column.  We have to handle both because
+      // a TripalEntity can have both types attached.
       $elements = explode('.', $field_name);
-      $bundle_term = array_shift($elements);
-      $field_name = array_shift($elements);
-      $element_name = implode(',', $elements);
-
-      // Get the field and instance.
-      $field = field_info_field($field_name);
-      $instance = field_info_instance('TripalEntity', $field_name, $this->query->entityConditions['bundle']['value']);
+      $field = NULL;
+      if (count($elements) > 2) {
+        $bundle_term = array_shift($elements);
+        $field_name = array_shift($elements);
+        // put the sub elements back together into a string with a 
+        // comma delimeter.
+        $element_name = implode(',', $elements);
+      }
+      if (count($elements) == 2) {
+        $field_name = array_shift($elements);
+        $element_name = array_shift($elements);
+        
+        // At this point we're still not 100% sure if we have a
+        // Tripal Storage API field or not.  One quick way to
+        // tell is to see if we get a field using the $field_name. if so the
+        // field name comes in as the second element.
+        $field = field_info_field($element_name);
+        if ($field) {
+          $field_name = $element_name;
+          $element_name = '';
+        }
+      }
+      
+      if ($field) { 
+        $instance = field_info_instance('TripalEntity', $field_name, $this->query->entityConditions['bundle']['value']);
 
-      // Construct the field term.
-      $field_term = $instance['settings']['term_vocabulary'] . ':' . $instance['settings']['term_accession'];
+        // Construct the field term.
+        $field_term = $instance['settings']['term_vocabulary'] . ':' . $instance['settings']['term_accession'];
 
-      // Let's add add on the $field_term to the element_name and add the
-      // query condition.
-      if ($element_name) {
-        $element_name = $field_term . ',' . $element_name;
+        // Let's add add on the $field_term to the element_name and add the
+        // query condition.
+        if ($element_name) {
+          $element_name = $field_term . ',' . $element_name;
+        }
+        else {
+          $element_name = $field_term;
+        }
+        $this->query->fieldCondition($field_name, $element_name, $value, $operator);
+        $this->cquery->fieldCondition($field_name, $element_name, $value, $operator);
       }
       else {
-        $element_name = $field_term;
+        // If we have a table name then this table is in the Drupal schema and
+        // we need to add a relationship with the tripal_entity table.
+        $table = $field_name;
+        $field = $element_name;
+        $this->query->relationshipCondition($table, $field, $value, $operator);
+        $this->cquery->relationshipCondition($table, $field, $value, $operator);
       }
-
-      $this->query->fieldCondition($field_name, $element_name, $value, $operator);
-      $this->cquery->fieldCondition($field_name, $element_name, $value, $operator);
     }
   }
-  
+ 
   /**
    * Add's a where exression clause to a query.
    * 
@@ -371,4 +400,22 @@ class tripal_views_query extends views_plugin_query {
     // are currently commented out.
     //return $this->query->placeholder($this->options['table'] . '_' . $this->options['field']);
   }
-}
+
+ 
+  /**
+   * This function copied from views_plugin_query_default::add_relationship
+   */ 
+  public function add_relationship($alias, $join, $base, $link_point = NULL) {
+  
+    // Make sure $alias isn't already used; if it, start adding stuff.
+    $alias_base = $alias;
+    $count = 1;
+    while (!empty($this->relationships[$alias])) {
+      $alias = $alias_base . '_' . $count++;
+    }
+      
+    $this->query->addRelationship($join->table, $alias, $join->field);
+    $this->cquery->addRelationship($join->table, $alias, $join->field);
+    return $alias;
+  }
+}

+ 6 - 8
tripal_chado/includes/tripal_chado.field_storage.inc

@@ -8,7 +8,9 @@ function tripal_chado_field_storage_info() {
     'field_chado_storage' => array(
       'label' => t('Chado'),
       'description' => t('Stores fields in the local Chado database.'),
-      'settings' => array(),
+      'settings' => array(
+        'tripal_storage_api' => TRUE,
+      ),
       // The logo_url key is supported by Tripal. It's not a Drupal key. It's
       // used for adding a logo or picture for the data store to help make it
       // more easily recognized on the  field_ui_field_overview_form. Ideally
@@ -741,16 +743,12 @@ function tripal_chado_field_storage_query($query) {
     $cquery->where('1=0');
   }
 
- //dpm($cquery->__toString());
- //dpm($cquery->getArguments());
+  // Please keep these dpm's they help with debugging
+  // dpm($cquery->__toString());
+  // dpm($cquery->getArguments());
 
   $records = $cquery->execute();
 
-  // If the query is a count query then just return the  total count.
-  if ($query->count) {
-    return $records->rowCount();
-  }
-
   // If this is not a count query then return the results.
   $result = array();
   while ($record = $records->fetchObject()) {