Browse Source

Merge pull request #439 from tripal/423-tv3-remote_data_field

Fix for remote__data field.
Lacey-Anne Sanderson 6 years ago
parent
commit
5787cef908

+ 73 - 35
tripal_ws/api/tripal_ws.api.inc

@@ -194,6 +194,36 @@ function tripal_remove_site($record_id) {
   return FALSE;
 }
 
+/**
+ * Constructs a URL for a remote Tripal web service.
+ * 
+ * @param $remote_site
+ *   A remote Tripal site object.
+ * @param $path
+ *   The web service path for the content (excluding
+ *   'web-servcies/vX.x/content').  To retrieve the full content listing
+ *   leave this paramter empty.
+ * @param $query
+ *   An query string to appear after the ? in a URL.
+ *   
+ * @return  
+ *   The full URL within the content service.
+ */
+function tripal_build_remote_content_url($remote_site, $path = '', $query = '') {
+  // Build the URL to the remote web services.
+  $ws_version = $remote_site->version;
+  $ws_url = $remote_site->url;
+  $ws_url = trim($ws_url, '/');
+  $ws_url .= '/web-services/content/' . $ws_version . '/' . $path;
+  
+  // Build the Query and make and substitions needed.
+  if ($query) {
+    $ws_url = $ws_url . '?' . $query;
+  }
+  
+  return $ws_url;
+}
+
 /**
  * Makes a request to the "content" service of a remote Tripal web site.
  *
@@ -225,57 +255,61 @@ function tripal_get_remote_content($site_id, $path = '', $query = '') {
     ->fetchObject();
 
   if (!$remote_site) {
-    tripal_report_error('tripal_ws', TRIPAL_ERROR,
-      t('Could not find a remote tripal site using the id provided: !id.',
-        array('!id' => $site_id)));
-    return FALSE;
-  }
-
-  // Build the URL to the remote web services.
-  $ws_version = $remote_site->version;
-  $ws_url = $remote_site->url;
-  $ws_url = trim($ws_url, '/');
-  $ws_url .= '/web-services/content/' . $ws_version . '/' . $path;
-
-  // Build the Query and make and substitions needed.
-  if ($query) {
-    $ws_url = $ws_url . '?' . $query;
-  }
-
-  // TODO: something is wrong here, the query is not being recognized on
-  // the remote Tripal site. It's just returning the default.
+    $data = [
+      'error' => t('Could not find a remote tripal site using the id provided: !id.',
+        array('!id' => $site_id))
+    ];
+    _tripal_report_ws_error($data);
+    return $data;
+  }
+  
+  // Make the remote query.
+  $ws_url = tripal_build_remote_content_url($remote_site, $path, $query);
   $data = drupal_http_request($ws_url);
-
   if (!$data) {
-    tripal_report_error('tripal_ws', TRIPAL_ERROR,
-        t('Could not connect to the remote web service.'));
-    return FALSE;
+    $data = [
+      'error' => t('Could not connect to the remote web service using the url: !url',
+        ['!url' => $ws_url])
+    ];
+    _tripal_report_ws_error($data);
+    return $data;
   }
 
   // If the data object has an error then this is some sort of
   // connection error (not a Tripal web servcies error).
   if (property_exists($data, 'error')) {
-    tripal_report_error('tripal_ws', TRIPAL_ERROR,
-        'Remote web Services reports the following error: !error. Using URL: !url',
-        array('!error' => $error, '!url' => $ws_url));
-    return FALSE;
+    $data = [
+      'error' => $data->error
+    ];
+    _tripal_report_ws_error($data);
+    return $data;
   }
 
   // We got a response, so convert it to a PHP array.
   $data = drupal_json_decode($data->data);
-
+  
   // Check if there was a Tripal Web Services error.
   if (array_key_exists('error', $data)) {
-    $error = '</pre>' . print_r($data['error'], TRUE) . '</pre>';
-    tripal_report_error('tripal_ws', TRIPAL_ERROR,
-        'Tripal remote web services reports the following error: !error. Using URL: !url',
-        array('!error' => $error, '!url' => $ws_url));
-    return FALSE;
+    _tripal_report_ws_error($data);
   }
 
   return $data;
 }
 
+/**
+ * A helper function for reporting an error when retrieving remote content.
+ * 
+ * @param $data
+ *   A data array containing at a minimum the 'error' key containing the
+ *   error message.
+ */
+function _tripal_report_ws_error($data) {  
+  $error = '</pre>' . print_r($data['error'], TRUE) . '</pre>';
+  tripal_report_error('tripal_ws', TRIPAL_ERROR,
+    'Tripal remote web services reports the following error: !error.',
+    array('!error' => $error));
+}
+
 /**
  * Retrieves the JSON-LD context for any remote Tripal web service.
  *
@@ -497,7 +531,9 @@ function tripal_load_remote_entities($remote_entity_ids, $site_id, $bundle_acces
     '&fields=' . urlencode(implode(",", $field_ids));
 
   $results = tripal_get_remote_content($site_id, $bundle_accession, $query);
-  if (!$results) {
+  
+  // If we encountered an error just return;
+  if (array_key_exists('error', $results)) {
     return FALSE;
   }
 
@@ -565,7 +601,9 @@ function tripal_load_remote_entity($remote_entity_id, $site_id, $bundle_accessio
 
   // Get the remote entity and create the fake entity.
   $remote_entity = tripal_get_remote_content($site_id, $bundle_accession . '/' . $remote_entity_id);
-  if (!$remote_entity) {
+  
+  // If we encountered an error just return;
+  if (array_key_exists('error', $results)) {
     return FALSE;
   }
 

+ 94 - 35
tripal_ws/includes/TripalFields/remote__data/remote__data.inc

@@ -89,7 +89,7 @@ class remote__data extends WebServicesField {
   // should exclude the field from web services or downloads.  An example
   // could be a quick search field that appears on the page that redirects
   // the user but otherwise provides no data.
-  public static $no_data = FALSE;
+  public static $no_data = TRUE;
 
   // Holds an object describing the remote site that tihs field connects to.
   private $remote_site = NULL;
@@ -105,9 +105,8 @@ class remote__data extends WebServicesField {
     // We don't want remote content to be available in web services.  There
     // is an if statement to not show this field in the web services but the
     // entity_load function doesn't know this field shouldn't be loaded so
-    // we need to short-circuit that.
-    $_SERVER['REQUEST_URI'];
-    if (preg_match('/^web-services/', $_SERVER['REQUEST_URI'])) {
+    // we need to short-circuit that.  
+    if (preg_match('/web-services/', $_SERVER['REQUEST_URI'])) {
       $this->loaded_via_ws = TRUE;
       return;
     }
@@ -128,22 +127,26 @@ class remote__data extends WebServicesField {
    * @see WebServicesField::load()
    */
   public function load($entity) {
+    
+    // If this field is being loaded via web services then just return.
+    if ($this->loaded_via_ws == TRUE) {
+      return;
+    }
 
     $field_name = $this->field['field_name'];
     $field_type = $this->field['type'];
 
     // Set some defaults for the empty record.
     $entity->{$field_name}['und'][0] = array(
-      'value' => array(),
-      'remote_entity' => array(),
+      'value' => '',
+      'remote_entity' => NULL,
+      'error' => FALSE,
+      'warning' => FALSE,
+      'admin_message' => '',
+      'query_str' => '',
     );
 
-    // If this field is being loaded via web services then just return.
-    if ($this->loaded_via_ws == TRUE) {
-      return;
-    }
-
-    // Get the query set by the admin for this field and replace any tokesn
+    // Get the query set by the admin for this field and replace any tokens
     $query_str = $this->instance['settings']['data_info']['query'];
     $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
     $query_str = tripal_replace_entity_tokens($query_str, $entity, $bundle);
@@ -151,23 +154,47 @@ class remote__data extends WebServicesField {
     // Make the request.
     $data = $this->makeRemoteRequest($query_str);
     if(!$data){
+      $entity->{$field_name}['und'][0]['value'] = 'ERROR: there was a problem retrieving content for this field.';
+      $entity->{$field_name}['und'][0]['admin_message'] =  "The remote service returned no data.";
+      $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
+      $entity->{$field_name}['und'][0]['error'] = TRUE;
+      $entity->{$field_name}['und'][0]['warning'] = FALSE;
+      $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_str);      
+      return;
+    }
+    // Make sure we didn't have a problem
+    if (array_key_exists('error', $data)) {
+      $entity->{$field_name}['und'][0]['value'] = 'ERROR: there was a problem retrieving content for this field.';
+      $entity->{$field_name}['und'][0]['admin_message'] = "The  content is currently not available because the " .
+          "remote service reported the following error: " . $data['error'] . ".";
+      $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
+      $entity->{$field_name}['und'][0]['error'] = TRUE;
+      $entity->{$field_name}['und'][0]['warning'] = FALSE;
+      $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_str);
       return;
     }
 
-    $total_items = $data['totalItems'];
-
+    $num_items = count($data['member']);
+    if ($num_items == 0) {
+      $entity->{$field_name}['und'][0]['value'] = 'Content is unavailable on the remote service.';
+      $entity->{$field_name}['und'][0]['admin_message'] = "The query to the remote service returned an empty result set. If you " .
+          "think this is an error, please check the query string and the remote service to verify. ";
+      $entity->{$field_name}['und'][0]['warning'] = TRUE;
+      $entity->{$field_name}['und'][0]['error'] = FALSE;
+      $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
+      $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_str);
+      return;
+    }
+    
     // Iterate through the members returned and save those for the field.
-    for ($i = 0; $i < count($data['members']); $i++) {
-      $member = $data['members'][$i];
+    for ($i = 0; $i < $num_items; $i++) {
+      $member = $data['member'][$i];
 
       // Get the cotent type and remote entity id
       $content_type = $member['@type'];
       $remote_entity_id = $member['@id'];
       $remote_entity_id = preg_replace('/^.*\/(\d+)/', '$1', $remote_entity_id);
 
-      // Save the member information for use later.
-      $entity->{$field_name}['und'][$i]['remote_entity'] = $member;
-
       // Separate the query_field if it has subfields.
       $rd_field_name = $this->instance['settings']['data_info']['rd_field_name'];
       $subfields = explode(',', $rd_field_name);
@@ -176,21 +203,42 @@ class remote__data extends WebServicesField {
       // Next get the the details about this member.
       $query_field_url =  $content_type . '/' . $remote_entity_id . '/' . $query_field;
       $field_data = $this->makeRemoteRequest($query_field_url);
-      if(!$field_data){
-        // If we encounter any type of error, we'll reset the field and return.
-        $entity->{$field_name}['und'] = array();
-        $entity->{$field_name}['und'][0] = array(
-          'value' => array(),
-          'remote_entity' => array(),
-        );
+      
+      // If we encounter any type of error, we'll reset the field and return.
+      if (array_key_exists('error', $field_data)) {
+        $entity->{$field_name} = [];
+        $entity->{$field_name}['und'][0]['value'] = 'ERROR: there was a problem retrieving secific content for this field.';
+        $entity->{$field_name}['und'][0]['admin_message'] = "While iterating through the list of results, the " .
+          "remote service reported the following error: " . $field_data['error'] . ". " ;
+        $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
+        $entity->{$field_name}['und'][0]['error'] = TRUE;
+        $entity->{$field_name}['und'][0]['warning'] = FALSE;
+        $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_field_url);
         return;
       }
-
+      
       // Set the field data as the value.
       $field_data_type = $field_data['@type'];
       $entity->{$field_name}['und'][$i]['value'] = $field_data;
+      $entity->{$field_name}['und'][$i]['remote_entity'] = $member;
+      $entity->{$field_name}['und'][$i]['error'] = FALSE;
+      $entity->{$field_name}['und'][$i]['warning'] = FALSE;
+      $entity->{$field_name}['und'][$i]['admin_message'] = '';
+      $entity->{$field_name}['und'][$i]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_field_url);;
     }
-   }
+  }
+  
+  /**
+   * Used to build the full URL for the query.
+   */
+  private function buildRemoteURL($remote_site, $query) {
+    $path = $query;
+    $q = '';
+    if (preg_match('/\?/', $query)) {
+      list($path, $q) = explode('?', $query);
+    }
+    return tripal_build_remote_content_url($remote_site, $path, $q);
+  }
    /**
     * Makes a request to a remote Tripal web services site.
     *
@@ -199,13 +247,12 @@ class remote__data extends WebServicesField {
     *   website.
     */
    private function makeRemoteRequest($query) {
-     $ctype = $query;
-     $qdata = '';
+     $path = $query;
+     $q = '';
      if (preg_match('/\?/', $query)) {
-       list($ctype, $qdata) = explode('?', $query);
+       list($path, $q) = explode('?', $query);
      }
-
-     $data = tripal_get_remote_content($this->remote_site->id, $query);
+     $data = tripal_get_remote_content($this->remote_site->id, $path, $q);
 
      return $data;
    }
@@ -259,8 +306,20 @@ class remote__data extends WebServicesField {
     $element['data_info']['query'] = array(
       '#type' => 'textarea',
       '#title' => 'Query to Execute',
-      '#description' => 'Build the query string that should be appended after the url. The tokens
-      listed below may be used in your query build.',
+      '#description' => 'Enter the query that will retreive the remote records. ' . 
+        'If the full URL to the content web service is ' .
+        'https://[tripal_site]/web-services/content/v0.1/. Then this field should ' .
+        'contain the text immediately after the content/v0.1 portion of the URL. ' .
+        'For information about building web services queries see the ' .
+        'online documentation at ' . l('The Tripal v3 User\'s Guide', 'http://tripal.info/tutorials/v3.x/web-services') . '. ' . 
+        'For example, suppose this field is attached to an ' .
+        'Organism content type on the local site, and you want to retrieve a ' .
+        'field for the same organism on a remote Tripal site then you will ' .
+        'want to query on the genus and species. Also, you want the genus and ' .
+        'species to match the organism that this field is attached to. You can ' .
+        'use tokens to do this (see the "Available Tokesn" fieldset below). ' .
+        'For this example, the query text should be ' . 
+        'Organism?genus=[taxrank__genus]&species=[taxrank__species].',
       '#default_value' => $this->instance['settings']['data_info']['query'],
       '#rows' => 5,
       '#required' => TRUE

+ 53 - 72
tripal_ws/includes/TripalFields/remote__data/remote__data_formatter.inc

@@ -11,64 +11,11 @@ class remote__data_formatter extends WebServicesFieldFormatter {
     'setting1' => 'default_value',
   );
   /**
-   * Provides the field's setting form.
-   *
-   * This function corresponds to the hook_field_formatter_settings_form()
-   * function of the Drupal Field API.
-   *
-   * The settings form appears on the 'Manage Display' page of the content
-   * type administration page. This function provides the form that will
-   * appear on that page.
-   *
-   * To add a validate function, please create a static function in the
-   * implementing class, and indicate that this function should be used
-   * in the form array that is returned by this function.
-   *
-   * This form will not be displayed if the formatter_settings_summary()
-   * function does not return anything.
-   *
-   * param $field
-   *   The field structure being configured.
-   * param $instance
-   *   The instance structure being configured.
-   * param $view_mode
-   *   The view mode being configured.
-   * param $form
-   *   The (entire) configuration form array, which will usually have no use
-   *   here.  Typically for reference only.
-   * param $form_state
-   *   The form state of the (entire) configuration form.
-   *
-   * @return
-   *   A Drupal Form array containing the settings form for this field.
-   */
-  public function settingsForm($view_mode, $form, &$form_state) {
-  }
-  /**
-   *  Provides the display for a field
-   *
-   * This function corresponds to the hook_field_formatter_view()
-   * function of the Drupal Field API.
-   *
-   *  This function provides the display for a field when it is viewed on
-   *  the web page.  The content returned by the formatter should only include
-   *  what is present in the $items[$delta]['values] array. This way, the
-   *  contents that are displayed on the page, via webservices and downloaded
-   *  into a CSV file will always be identical.  The view need not show all
-   *  of the data in the 'values' array.
-   *
-   *  @param $element
-   *  @param $entity_type
-   *  @param $entity
-   *  @param $langcode
-   *  @param $items
-   *  @param $display
-   *
-   *  @return
-   *    An element array compatible with that returned by the
-   *    hook_field_formatter_view() function.
+   * @see TripalFieldFormatter::view()
    */
   public function view(&$element, $entity_type, $entity, $langcode, $items, $display) {
+    $content = '';
+    
     // Get the settings
     $settings = $display['settings'];
     $field_name = $this->field['field_name'];
@@ -94,25 +41,39 @@ class remote__data_formatter extends WebServicesFieldFormatter {
       ->execute()
       ->fetchObject();
 
-      $content = '<p>';
-    if (is_object($site_logo)) {
-      $content .= '<img class="tripal-remote--data-field-logo" src="' . file_create_url($site_logo->uri) . '"><br/>';
-    }
-    $content .=  t('This content provided by !site.',
-        array('!site' => l($site->name, $site->url, array('attributes' => array("target" => '_blank')))));
-    $content .= '</p>';
+    // Iterate through the results and create a generic table.
     $rows = array();
+    $headers = array('');
     foreach ($items as $index => $item) {
-      $remote_entity_label = $item['remote_entity']['label'];
-      $remote_entity_page = $item['remote_entity']['ItemPage'];
-      $link = t('View !data on %site',
-          array('!data' => l('this content', $remote_entity_page, array('attributes' => array('target' => '_blank'))),
-            '%site' => $site->name));
+      if (!$item['value'] or empty($item['value'])) {
+        continue;
+      }
       $value = $item['value'];
-      if (!$value) {
+      $error = $item['error'];
+      $warning = $item['warning'];
+      
+      // If there is an error or warning then clear the cache for this field
+      // so that next time the page is loaded it will try to reload again.
+      if ($error or $warning) {
+        $cid = "field:TripalEntity:" . $entity->id . ':' . $field_name;
+        cache_clear_all($cid, 'cache_field');
+        if ($item['admin_message']) {
+          $severity = TRIPAL_ERROR;
+          if ($warning) {
+            $severity = TRIPAL_WARNING;
+          }
+          $value .= tripal_set_message($item['admin_message'] . 'The query URL was: ' . l($item['query_str'], $item['query_str'], ['attributes' => ['target' => '_blank']]),
+            $severity, ['return_html' => TRUE]);
+        }
+        $rows[] = [$value];
         continue;
       }
-      $headers = array('');
+      
+      $remote_entity_label = array_key_exists('label', $item) ? $item['remote_entity']['label'] : '';
+      $remote_entity_page = $item['remote_entity']['ItemPage'];
+      $remote_entity_link = t('View !data on %site',
+          array('!data' => l('this data', $remote_entity_page, array('attributes' => array('target' => '_blank'))),
+            '%site' => $site->name));
 
       // If this is a collection then handle it as a list of members.
       if (array_key_exists('members', $value)) {
@@ -128,7 +89,7 @@ class remote__data_formatter extends WebServicesFieldFormatter {
         }
         else {
           if (array_key_exists($flabel, $value)) {
-            $rows[] = array(l($value[$flabel], $remote_entity_page, array('attributes' => array('target' => '_blank'))));
+            $rows[] = array($value[$flabel]);
           }
           else {
             $value['Link'] = l('View content on ' . $site->name, $remote_entity_page, array('attributes' => array('target' => '_blank')));
@@ -136,8 +97,9 @@ class remote__data_formatter extends WebServicesFieldFormatter {
           }
         }
       }
-
     }
+    
+    // TODO: we need to handle paged elements.
 
     $has_sub_tables = FALSE;
     for ($i = 0; $i < count($rows); $i++) {
@@ -145,6 +107,12 @@ class remote__data_formatter extends WebServicesFieldFormatter {
         $rows[$i][0] = $this->createTable($rows[$i]);
         $has_sub_tables = TRUE;
       }
+      else {
+        $rows[$i] = [
+          'colspan' => 2,
+          'data' => $rows[$i],
+        ];
+      }
     }
 
     // If we don't have  tables for each row then we'll put everything into
@@ -171,6 +139,19 @@ class remote__data_formatter extends WebServicesFieldFormatter {
         $content .= $rows[$i][0];
       }
     }
+    
+    $content .= '<p>';
+
+    $content .=  t('This content provided by !site.',
+      array('!site' => l($site->name, $site->url, array('attributes' => array("target" => '_blank')))));
+    if (is_object($site_logo)) {
+      $content .= '<img class="tripal-remote--data-field-logo" src="' . file_create_url($site_logo->uri) . '"><br/>';
+    }
+    if (count($items) == 1) {
+      $content .= $remote_entity_link;
+    }
+    $content .= '</p>';
+    
     // Return the content for this field.
     $element[0] = array(
       '#type' => 'markup',

+ 18 - 8
tripal_ws/includes/TripalWebService/TripalContentService_v0_1.inc

@@ -68,10 +68,14 @@ class TripalContentService_v0_1 extends TripalWebService {
       $ctype_lookup = array();
       $found = FALSE;
       while ($bundle = $bundles->fetchObject()) {
-        if ($ctype == preg_replace('/[^\w]/', '_', $bundle->label)) {
+        // Check the label by replacing non alpha-numeric characters with 
+        // an underscore and is case-insensitive
+        $label = preg_replace('/[^\w]/', '_', $bundle->label);
+        if (preg_match("/^$label$/i", $ctype)) {
           $ctype = $bundle->label;
           $found = TRUE;
         }
+        // Check if this is an accession.
         if ($ctype == $bundle->vocabulary . ':' . $bundle->accession) {
           $ctype = $bundle->label;
           $found = TRUE;
@@ -274,8 +278,16 @@ class TripalContentService_v0_1 extends TripalWebService {
       }
       // Get the information about this field.
       $field = field_info_field($field_name);
+      
+      // If the field has the $no_data turned on then we should exclude it.
+      if (tripal_load_include_field_class($field['type'])) {
+        $field_class = $field['type'];
+        if ($field_class::$no_data) {
+          return;
+        }
+      }
 
-      // Skip the remote__data field that is provided by the tripal_Ws
+      // Skip the remote__data field that is provided by the tripal_ws
       // module.
       if ($field['type'] == 'remote__data') {
         continue;
@@ -946,12 +958,10 @@ class TripalContentService_v0_1 extends TripalWebService {
       $entity = $entity[$entity_id];
 
       // Add in any requested fields
-      foreach ($fields as $expfield) {
-        if (array_key_exists($expfield, $add_fields)) {
-          $this->addEntityField($member, $add_fields[$expfield]['term'], $entity,
-              $bundle, $add_fields[$expfield]['field'], $add_fields[$expfield]['instance'],
-              $service_path);
-        }
+      foreach ($add_fields as $expfield => $expfield_details) {
+        $this->addEntityField($member, $expfield_details['term'], $entity,
+            $bundle, $expfield_details['field'], $expfield_details['instance'],
+            $service_path);
       }
       $this->resource->addMember($member);
     }

+ 2 - 2
tripal_ws/includes/tripal_ws.admin.inc

@@ -98,14 +98,14 @@ function tripal_ws_tripal_sites_edit_form($form, &$form_state, $tripal_site_id =
   $form['tripal_site_info']['url'] = array(
     '#title' => t('URL'),
     '#type' => 'textfield',
-    '#description' => t('The URL of the Tripal site.'),
+    '#description' => t('The URL of the Tripal site, including the "http://" or "https://" followed by the address for the site\'s home page.'),
     '#default_value' => $url,
     '#required' => TRUE
   );
   $form['tripal_site_info']['version'] = array(
     '#title' => t('Version'),
     '#type' => 'textfield',
-    '#description' => t('Web services version used by the Tripal site.'),
+    '#description' => t('Web services version used by the Tripal site (example: v0.1)'),
     '#default_value' => $version,
   );
   $form['tripal_site_info']['description'] = array(

+ 1 - 0
tripal_ws/theme/css/tripal_ws.css

@@ -5,6 +5,7 @@
 }
 .tripal-remote--data-field-logo {
   max-height: 100px;
+  max-width: 150px;
 }
 
 .tripal-remote--data-field-dl dt {