Browse Source

Move type_options and validate in separate methods for code readability, testing and debugging purposes.

Lacey Sanderson 6 years ago
parent
commit
bef237dd7a

+ 197 - 125
tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship.inc

@@ -379,7 +379,7 @@ class sbo__relationship extends ChadoField {
     $pkey = $this->schema['primary key'][0];
     $subject_id_key = $this->subject_id_column;
     $object_id_key = $this->object_id_column;
-    // @todo grad these separately like it was before.
+    // @todo grab these separately like it was before.
     $subject_pkey = $object_pkey = $this->base_schema['primary key'][0];
 
     $entity->{$field_name}['und'][$delta]['value'] = array(
@@ -880,153 +880,211 @@ class sbo__relationship extends ChadoField {
     $field_column = $this->instance['settings']['chado_column'];
     $base_table = $this->instance['settings']['base_table'];
 
-    $schema = chado_get_schema($field_table);
-    $fkeys = $schema['foreign keys'];
+    // Grab the chado record_id for this entity.
+    $chado_record_id = NULL;
+    if ($entity AND isset($entity->chado_record_id)) {
+     $chado_record_id = $entity->chado_record_id;
+    }
+
+    // Validate each releationship.
+    foreach ($items as $delta => $item) {
+      $item_errors = $this->validateItem($item, $chado_record_id);
+      if (!empty($item_errors)) {
+        $errors[$field_name][$delta][$langcode] = $item_errors;
+      }
+    }
+  }
+
+  /**
+   * Validate a Single relationship.
+   *
+   * @param $item
+   *   A single item from the $items array passed to TripalField::validate().
+   * @return
+   *   An array of errors where each has a:
+   *     - error: this is an error code which is the name of the field.
+   *     - message: A message to show the user describing the problem.
+   */
+  public function validateItem($item, $chado_record_id = NULL) {
+    $errors = array();
+
+    $field_name = $this->field['field_name'];
+    $field_type = $this->field['type'];
+    $field_table = $this->instance['settings']['chado_table'];
+    $field_column = $this->instance['settings']['chado_column'];
+    $base_table = $this->instance['settings']['base_table'];
 
     // 'nd_reagent_relationship' and 'project_relationship' have different column names from
     // subject_id/object_id. Do a pattern matching to get the column names.
-    $subject_id_key = 'subject_id';
-    $object_id_key = 'object_id';
-    foreach ($schema['foreign keys'][$base_table]['columns'] AS $lcolum => $rcolum) {
-      if (preg_match('/^subject_.*id/', $lcolum)) {
-        $subject_id_key = $lcolum;
-      }
-      else if (preg_match('/^object_.*id/', $lcolum)) {
-        $object_id_key = $lcolum;
-      }
+    $subject_id_key = $this->subject_id_column;
+    $object_id_key = $this->object_id_column;
+
+    $subject_id = $item['chado-' . $field_table . '__' . $subject_id_key];
+    $object_id = $item['chado-' . $field_table . '__' . $object_id_key];
+    $type_id = $item['chado-' . $field_table . '__type_id'];
+    $type_id = isset($item['type_id']) ? $item['chado-' . $field_table . '__type_id'] : $type_id;
+    $type_name = isset($item['type_name']) ? $item['type_name'] : '';
+    $voc_id = isset($item['vocabulary']) ? $item['vocabulary'] : '';
+    $subject_name = $item['subject_name'];
+    $object_name = $item['object_name'];
+
+    // If the row is empty then just continue, there's nothing to validate.
+    if (!$type_id and !$type_name and !$subject_name and !$object_name) {
+      return;
     }
 
-    foreach ($items as $delta => $item) {
-      $subject_id = $item['chado-' . $field_table . '__' . $subject_id_key];
-      $object_id = $item['chado-' . $field_table . '__' . $object_id_key];
-      $type_id = $item['chado-' . $field_table . '__type_id'];
-      $type_id = isset($item['type_id']) ? $item['chado-' . $field_table . '__type_id'] : $type_id;
-      $type_name = isset($item['type_name']) ? $item['type_name'] : '';
-      $subject_name = $item['subject_name'];
-      $object_name = $item['object_name'];
-
-
-      // If the row is empty then just continue, there's nothing to validate.
-      if (!$type_id and !$type_name and !$subject_name and !$object_name) {
-        continue;
-      }
+    // Check: Make sure we have values for all of the fields.
+    if (!$type_name && !$type_id) {
+      $errors[] = array(
+        'error' => 'sbo__relationship',
+        'message' => t("Please provide the type of relationship."),
+        'element' => 'type',
+      );
+    }
+    if (!$subject_name) {
+      $errors[] = array(
+        'error' => 'sbo__relationship',
+        'message' => t("Please provide the subject of the relationship."),
+        'element' => 'subject',
+      );
+    }
+    if (!$object_name) {
+      $errors[] = array(
+        'error' => 'sbo__relationship',
+        'message' => t("Please provide the object of the relationship."),
+        'element' => 'object',
+      );
+    }
+
+    // Check: Cvterm exists.
+    if (!$type_id AND !$type_name) {
+      $errors[] = array(
+        'error' => 'sbo__relationship',
+        'message' => t("We were unable to find the type you specified. Please check spelling and that the term already exists."),
+        'element' => 'type',
+      );
+    }
+    elseif ($type_name AND $voc_id) {
+      $val = array(
+        'cv_id' => $voc_id,
+        'name' => $type_name
+      );
+      $cvterm = chado_generate_var('cvterm', $val);
 
-      // Make sure we have values for all of the fields.
-      $form_error = FALSE;
-      if (!$type_name && !$type_id) {
-        $errors[$field_name][$delta]['und'][] = array(
+      if (!isset($cvterm->cvterm_id)) {
+
+        $errors[] = array(
           'error' => 'sbo__relationship',
-          'message' => t("Please provide the type of relationship."),
+          'message' => t("We were unable to find the type you specified. Please check spelling and that the term already exists."),
+          'element' => 'type',
         );
+
       }
-      if ($entity and !$subject_name) {
-        $errors[$field_name][$delta]['und'][] = array(
+    }
+
+
+    // Before submitting this form we need to make sure that our subject_id and
+    // object_ids are real records.  There are two ways to get the record, either
+    // just with the text value or with an [id: \d+] string embedded.  If the
+    // later we will pull it out.
+    $subject_id = '';
+    $fkey_rcolumn = $this->schema['foreign keys'][$base_table]['columns'][$subject_id_key];
+    $matches = array();
+    if(preg_match('/\[id: (\d+)\]/', $subject_name, $matches)) {
+      $subject_id =  $matches[1];
+      $values = array($fkey_rcolumn => $subject_id);
+      $subject = chado_select_record($base_table, array($fkey_rcolumn), $values);
+      if (count($subject) == 0) {
+        $errors[] = array(
           'error' => 'sbo__relationship',
-          'message' => t("Please provide the subject of the relationship."),
+          'message' => t("The subject record cannot be found using the specified id (e.g. [id: xx])."),
+          'element' => 'subject',
         );
       }
-      if ($entity and !$object_name) {
-        $errors[$field_name][$delta]['und'][] = array(
+    }
+    else {
+      // Otherwise we need to look it up using the name field determined in the
+      // constructor for the current field. There may be more then one name field
+      // (e.g. organism: genus + species) so we want to check both.
+      $sql = 'SELECT ' . $fkey_rcolumn . ' FROM {' . $base_table . '} WHERE ' . implode('||', $this->base_name_columns) . '=:keyword';
+      $subject = chado_query($sql, [':keyword' => $subject_name])->fetchAll();
+      if (count($subject) == 0 AND $chado_record_id) {
+        $errors[] = array(
           'error' => 'sbo__relationship',
-          'message' => t("Please provide the object of the relationship."),
+          'message' => t("The subject record cannot be found. Please check spelling."),
+          'element' => 'subject',
         );
       }
-      if ($form_error) {
-        continue;
+      elseif (count($subject) > 1) {
+        $errors[] = array(
+          'error' => 'sbo__relationship',
+          'message' => t("The subject is not unique and therefore the relationship cannot be made."),
+          'element' => 'subject',
+        );
       }
+    }
 
-      // Before submitting this form we need to make sure that our subject_id and
-      // object_ids are real records.  There are two ways to get the record, either
-      // just with the text value or with an [id: \d+] string embedded.  If the
-      // later we will pull it out.
-      $subject_id = '';
-      $fkey_rcolumn = $fkeys[$base_table]['columns'][$subject_id_key];
-      $matches = array();
-      if ($entity) {
-        if(preg_match('/\[id: (\d+)\]/', $subject_name, $matches)) {
-          $subject_id =  $matches[1];
-          $values = array($fkey_rcolumn => $subject_id);
-          $subject = chado_select_record($base_table, array($fkey_rcolumn), $values);
-          if (count($subject) == 0) {
-            $errors[$field_name][$delta]['und'][] = array(
-              'error' => 'sbo__relationship',
-              'message' => t("The subject record cannot be found using the specified id (e.g. [id: xx])."),
-            );
-          }
-        }
-        else {
-          /* @uniquename there is not always a uniquename */
-          $values = array('uniquename' => $subject_name);
-          $subject = chado_select_record($base_table, array($fkey_rcolumn), $values);
-          if (count($subject) == 0) {
-            $errors[$field_name][$delta]['und'][] = array(
-              'error' => 'sbo__relationship',
-              'message' => t("The subject record cannot be found. Please check spelling."),
-            );
-          }
-          elseif (count($subject) > 1) {
-            $errors[$field_name][$delta]['und'][] = array(
-              'error' => 'sbo__relationship',
-              'message' => t("The subject is not unique and therefore the relationship cannot be made."),
-            );
-          }
-        }
+    // Now check for a matching object.
+    $object_id = '';
+    $fkey_rcolumn = $this->schema['foreign keys'][$base_table]['columns'][$object_id_key];
+    $matches = array();
+    if (preg_match('/\[id: (\d+)\]/', $object_name, $matches)) {
+      $object_id = $matches[1];
+      $values = array($fkey_rcolumn => $object_id);
+      $object = chado_select_record($base_table, array($fkey_rcolumn), $values);
+      if (count($subject) == 0 AND $chado_record_id) {
+        $errors[] = array(
+          'error' => 'sbo__relationship',
+          'message' => t("The object record cannot be found using the specified id (e.g. [id: xx])."),
+          'element' => 'object',
+        );
       }
-
-      // Now check for a matching object.
-      $object_id = '';
-      $fkey_rcolumn = $fkeys[$base_table]['columns'][$object_id_key];
-      $matches = array();
-      if ($entity) {
-        if (preg_match('/\[id: (\d+)\]/', $object_name, $matches)) {
-          $object_id = $matches[1];
-          $values = array($fkey_rcolumn => $object_id);
-          $object = chado_select_record($base_table, array($fkey_rcolumn), $values);
-          if (count($subject) == 0) {
-            $errors[$field_name][$delta]['und'][] = array(
-              'error' => 'sbo__relationship',
-              'message' => t("The object record cannot be found using the specified id (e.g. [id: xx])."),
-            );
-          }
-        }
-        else {
-          /* @uniquename there is not always a uniquename */
-          $values = array('uniquename' => $object_name);
-          $object = chado_select_record($base_table, array($fkey_rcolumn), $values);
-          if (count($object) == 0) {
-            $errors[$field_name][$delta]['und'][] = array(
-              'error' => 'sbo__relationship',
-              'message' => t("The object record cannot be found. Please check spelling."),
-            );;
-          }
-          elseif (count($object) > 1) {
-            $errors[$field_name][$delta]['und'][] = array(
-              'error' => 'sbo__relationship',
-              'message' =>  t("The object is not unique and therefore the relationship cannot be made."),
-            );
-          }
-        }
+    }
+    else {
+      // Otherwise we need to look it up using the name field determined in the
+      // constructor for the current field. There may be more then one name field
+      // (e.g. organism: genus + species) so we want to check both.
+      $sql = 'SELECT ' . $fkey_rcolumn . ' FROM {' . $base_table . '} WHERE ' . implode('||', $this->base_name_columns) . '=:keyword';
+      $object = chado_query($sql, [':keyword' => $object_name]);
+      if (count($object) == 0) {
+        $errors[] = array(
+          'error' => 'sbo__relationship',
+          'message' => t("The object record cannot be found. Please check spelling."),
+          'element' => 'object',
+        );
       }
+      elseif (count($object) > 1) {
+        $errors[] = array(
+          'error' => 'sbo__relationship',
+          'message' =>  t("The object is not unique and therefore the relationship cannot be made."),
+          'element' => 'object',
+        );
+      }
+    }
 
-      // Make sure that either our object or our subject refers to the base record.
-      if ($entity) {
-        $chado_record_id = $entity->chado_record_id;
-        if ($object_id != $chado_record_id  and $subject_id != $chado_record_id) {
-          $errors[$field_name][$delta]['und'][] = array(
-            'error' => 'sbo__relationship',
-            'message' =>  t("Either the subject or the object in the relationship must refer to this record."),
-          );
-        }
+    // Make sure that either our object or our subject refers to the base record.
+    if ($object_id AND $subject_id) {
+      if ($object_id != $chado_record_id  and $subject_id != $chado_record_id) {
+        $errors[] = array(
+          'error' => 'sbo__relationship',
+          'message' =>  t("Either the subject or the object in the relationship must refer to this record."),
+          'element' => 'row',
+        );
+      }
+    }
 
-        // Make sure that the object and subject are not both the same thing.
-        if ($object_id == $subject_id) {
-          $errors[$field_name][$delta]['und'][] = array(
-            'error' => 'sbo__relationship',
-            'message' =>  t("The subject and the object in the relationship cannot both refer to the same record."),
-          );
-        }
+    // Make sure that the object and subject are not both the same thing.
+    if ($object_id AND $subject_id) {
+      if ($object_id == $subject_id) {
+        $errors[] = array(
+          'error' => 'sbo__relationship',
+          'message' =>  t("The subject and the object in the relationship cannot both refer to the same record."),
+            'element' => 'row',
+        );
       }
     }
+
+    return $errors;
   }
 
 
@@ -1057,4 +1115,18 @@ class sbo__relationship extends ChadoField {
   public function getObjectIdColumn() {
     return $this->object_id_column;
   }
+
+  /**
+   * Retrieve the base name columns.
+   */
+  public function getBaseNameColumns() {
+    return $this->base_name_columns;
+  }
+
+  /**
+   * Retrieve the base type column.
+   */
+  public function getBaseTypeColumn() {
+    return $this->base_type_column;
+  }
 }

+ 270 - 198
tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship_widget.inc

@@ -24,6 +24,15 @@ class sbo__relationship_widget extends ChadoFieldWidget {
   protected $subject_id_column;
   protected $object_id_column;
 
+  // An array of columns to use as the "name" of the subject and object.
+  // For example, for the feature table, this will be the uniquename,
+  // whereas, for the organism table this will be the genus & species.
+  protected $base_name_columns;
+
+  // One of 'type_id', or 'table_name'. Not all base tables have a type_id so
+  // this setting allows us to better handle these cases.
+  protected $base_type_column;
+
   // The field instance for this widget. This allows us to use some of the
   // field methods and info in the widget.
   protected $field_instance;
@@ -45,6 +54,10 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     $this->subject_id_column = $this->field_instance->getSubjectIdColumn();
     $this->object_id_column = $this->field_instance->getObjectIdColumn();
 
+    // Retrieve the columns to use for name/type.
+    $this->base_name_columns = $this->field_instance->getBaseNameColumns();
+    $this->base_type_column = $this->field_instance->getBaseTypeColumn();
+
   }
 
   /**
@@ -63,34 +76,19 @@ class sbo__relationship_widget extends ChadoFieldWidget {
 
     // @debug dpm($this, 'this');
 
-    // Get the primary key of the base table
-    $pkey = $this->base_schema['primary key'][0];
-
-    // Get the instance settings. There are three options for how this widget
-    // will be displayed. Those are controlled in the instance settings
-    // of the field.
-    // Option 1:  relationship types are limited to a specific vocabulary.
-    // Option 2:  relationship types are limited to a subset of one vocabulary.
-    // Option 3:  relationship types are limited to a predefined set.
-    $instance = $this->instance;
-    $settings = '';
-    $option1_vocabs = '';
-    $option2_parent = '';
-    $option2_vocab = '';
-    $option3_rtypes  = '';
-    if (array_key_exists('relationships', $instance)) {
-      $settings = $instance['settings']['relationships'];
-      $option1_vocabs = $settings['option1_vocabs'];
-      $option2_vocab  = $settings['option2_vocab'];
-      $option2_parent = $settings['option2_parent'];
-      $option3_rtypes = $settings['relationship_types'];
-    }
+    // Get the primary key of the relationship table
+    $pkey = $this->schema['primary key'][0];
 
-    // For testing if there are selected vocabs for option1 we'll copy the
-    // contents in a special variable for later.
-    $option1_test = $option1_vocabs;
+    // 'nd_reagent_relationship' and 'project_relationship' have different column names from
+    // subject_id/object_id. Retrieve those determined in the constructor.
+    $subject_id_key = $this->subject_id_column;
+    $object_id_key = $this->object_id_column;
+    // And save them in the widget for use in testing/debugging.
+    $widget['#subject_id_key'] = $subject_id_key;
+    $widget['#object_id_key'] = $object_id_key;
 
-    // Get the field defaults.
+    // Default Values:
+    //----------------
     $record_id = '';
     $subject_id = '';
     $object_id = '';
@@ -101,14 +99,6 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     $object_uniquename = '';
     $type = '';
 
-    // 'nd_reagent_relationship' and 'project_relationship' have different column names from
-    // subject_id/object_id. Retrieve those determined in the constructor.
-    $subject_id_key = $this->subject_id_column;
-    $object_id_key = $this->object_id_column;
-    // And save them in the widget for use in testing/debugging.
-    $widget['#subject_id_key'] = $subject_id_key;
-    $widget['#object_id_key'] = $object_id_key;
-
     // If the field already has a value then it will come through the $items
     // array.  This happens when editing an existing record.
     if (count($items) > 0 and array_key_exists($delta, $items)) {
@@ -162,11 +152,27 @@ class sbo__relationship_widget extends ChadoFieldWidget {
       //@debug dpm(array($subject_id, $type_id, $object_id), 'have an item (AJAX)!');
     }
 
+    // Getting default values for the relationship type element.
+    $default_voc = '';
+    if (isset($form_state['field'][$field_name]['und']['instance']['default_value'][0]['vocabulary'])) {
+      $default_voc = $form_state['field'][$field_name]['und']['instance']['default_value'][0]['vocabulary'];
+    }
+    $default_term = '';
+    if (isset($form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_name'])) {
+      $default_term = $form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_name'];
+    }
+
+    $default_type_id = $type_id;
+    if (!$type_id && isset($form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_id'])) {
+      $default_type_id = $form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_id'];
+    }
+
     // Check if we have autocomplete available for this base table
     $autocomplete_path = "admin/tripal/storage/chado/auto_name/$base_table";
     $has_autocomplete = db_query('SELECT 1 FROM menu_router WHERE path=:path',
       array(':path' => $autocomplete_path.'/%'))->fetchField();
 
+    // Save some values for later...
     $widget['#table_name'] = $field_table;
 
     $widget['#fkeys'] = $this->schema['foreign keys'];
@@ -176,6 +182,8 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     $widget['#prefix'] =  "<span id='$field_table-$delta'>";
     $widget['#suffix'] =  "</span>";
 
+    // Save the values needed by the Chado Storage API.
+    //-------------------------------------------------
     $widget['value'] = array(
       '#type' => 'value',
       '#value' => array_key_exists($delta, $items) ? $items[$delta]['value'] : '',
@@ -208,6 +216,9 @@ class sbo__relationship_widget extends ChadoFieldWidget {
         '#default_value' => $rank,
       );
     }
+
+    // Subject:
+    //----------
     $widget['subject_name'] = array(
       '#type' => 'textfield',
       '#title' => t('Subject'),
@@ -221,109 +232,16 @@ class sbo__relationship_widget extends ChadoFieldWidget {
       $widget['subject_name']['#autocomplete_path'] = $autocomplete_path;
     }
 
-    // Getting default values for the relationship type element.
-    $default_voc = '';
-    if (isset($form_state['field'][$field_name]['und']['instance']['default_value'][0]['vocabulary'])) {
-      $default_voc = $form_state['field'][$field_name]['und']['instance']['default_value'][0]['vocabulary'];
-    }
-    $default_term = '';
-    if (isset($form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_name'])) {
-      $default_term = $form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_name'];
-    }
+    // Type:
+    //-------
+    $rtype_options = $this->get_rtype_select_options();
+    if ($rtype_options) {
 
-    $default_type_id = $type_id;
-    if (!$type_id && isset($form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_id'])) {
-      $default_type_id = $form_state['field'][$field_name]['und']['instance']['default_value'][0]['type_id'];
-    }
-    // Option 3: Custom list of Relationship Types
-    $rtype_options = array();
-    $rtype_options[] = 'Select a Type';
-    if ($option3_rtypes) {
-      $rtypes = explode(PHP_EOL, $option3_rtypes);
-      foreach($rtypes AS $rtype) {
-        // Ignore empty lines
-        if (trim($rtype) == '') {
-          continue;
-        }
-        $term = chado_get_cvterm(array('name' => trim($rtype)));
-        // Try to get term with vocabulary specified
-        if (!$term) {
-          $tmp = explode('|', trim($rtype), 2);
-          $cv = chado_get_cv(array('name' => trim($tmp[0])));
-          $rtype = trim($tmp[1]);
-          $term = chado_get_cvterm(array('name' => $rtype, 'cv_id' => $cv->cv_id));
-        }
-        $rtype_options[$term->cvterm_id] = $term->name;
-      }
-      $widget['type_id'] = array(
-        '#type' => 'select',
-        '#title' => t('Relationship Type'),
-        '#options' => $rtype_options,
-        '#default_value' => $default_type_id,
-      );
-      if ($type_id && !key_exists($type_id, $rtype_options)) {
-        form_set_error($this->field['field_name'] . '[' . $langcode . '][' . $delta . '][type_id]', 'Illegal option detected for Relationship Type. Please contact site administrator to fix the problem');
-      }
-    }
-    // Option 2: Child terms of a selected cvterm
-    else if ($option2_vocab) {
-      $values = array(
-        'cv_id' => $option2_vocab,
-        'name' => $option2_parent
-      );
-      $parent_term = chado_get_cvterm($values);
-
-      // If the term wasn't found then see if it's a synonym.
-      if(!$parent_term) {
-        $values = array(
-          'synonym' => array(
-            'name' => trim($option2_parent),
-          )
-        );
-        $synonym = chado_get_cvterm($values);
-        if ($synonym && $synonym->cv_id->cv_id == $option2_vocab) {
-          $parent_term = $synonym;
-        }
-      }
-      // Get the child terms of the parent term found above.
-      $sql = "
-        SELECT subject_id,
-          (SELECT name from {cvterm} where cvterm_id = subject_id) AS name
-        FROM {cvtermpath}
-        WHERE
-          object_id = :parent_cvterm_id AND
-          cv_id = :parent_cv_id
-        ORDER BY name
-       ";
-      $args = array(
-        ':parent_cvterm_id' => $parent_term->cvterm_id,
-        ':parent_cv_id' => $parent_term->cv_id->cv_id
-      );
-      $results = chado_query($sql, $args);
-      while($child = $results->fetchObject()) {
-        $rtype_options[$child->subject_id] = $child->name;
-      }
-      $widget['type_id'] = array(
-        '#type' => 'select',
-        '#title' => t('Relationship Type'),
-        '#options' => $rtype_options,
-        '#default_value' => $default_type_id,
-      );
-      if ($type_id && !key_exists($type_id, $rtype_options)) {
-        form_set_error($this->field['field_name'] . '[' . $langcode . '][' . $delta . '][type_id]', 'Illegal option detected for Relationship Type. Please contact site administrator to fix the problem');
-      }
-    }
-    // Option 1: All terms of selected vocabularies
-    else if ($option1_test && array_pop($option1_test)) {
-      $sql = "SELECT cvterm_id, name FROM {cvterm} WHERE cv_id IN (:cv_id) ORDER BY name";
-      $results = chado_query($sql, array(':cv_id' => $option1_vocabs));
-      while ($obj = $results->fetchObject()) {
-        $rtype_options[$obj->cvterm_id] = $obj->name;
-      }
       $widget['type_id'] = array(
         '#type' => 'select',
         '#title' => t('Relationship Type'),
         '#options' => $rtype_options,
+        '#empty_option' => 'Select a Type',
         '#default_value' => $default_type_id,
       );
       if ($type_id && !key_exists($type_id, $rtype_options)) {
@@ -331,11 +249,14 @@ class sbo__relationship_widget extends ChadoFieldWidget {
       }
     }
     // Default option:
+    // If we were determine an type_id option set...
+    // then we will need to provide a cv + type autocomplete.
     else {
       // Set up available cvterms for selection
-      $vocs = array(0 => 'Select a vocabulary');
+      $vocs = array();
       $vocs = chado_get_cv_select_options();
-      $cv_id = isset($form_state['values'][$field_name]['und'][0]['vocabulary']) ? $form_state['values'][$field_name]['und'][0]['vocabulary'] : 0;
+      unset($vocs[0]);
+      $cv_id = isset($form_state['values'][$field_name][$langcode][$delta]['vocabulary']) ? $form_state['values'][$field_name][$langcode][$delta]['vocabulary'] : 0;
       // Try getting the cv_id from cvterm for existing records
       if (!$cv_id && $type_id) {
         $cvterm = chado_get_cvterm(array('cvterm_id' => $type_id));
@@ -353,6 +274,7 @@ class sbo__relationship_widget extends ChadoFieldWidget {
         '#options' => $vocs,
         '#required' => $element['#required'],
         '#default_value' => $cv_id,
+        '#empty_option' => 'Select a Vocabulary',
         '#ajax' => array(
           'callback' => "sbo__relationship_widget_form_ajax_callback",
           'wrapper' => "$field_table-$delta",
@@ -373,6 +295,8 @@ class sbo__relationship_widget extends ChadoFieldWidget {
       }
     }
 
+    // Object:
+    //--------
     $widget['object_name'] = array(
       '#type' => 'textfield',
       '#title' => t('Object'),
@@ -406,8 +330,8 @@ class sbo__relationship_widget extends ChadoFieldWidget {
 
     // 'nd_reagent_relationship' and 'project_relationship' have different column names from
     // subject_id/object_id. Retrieve the column names determined in the form.
-    $subject_id_key = $form[$field_name][$langcode][$delta]['#subject_id_key'];
-    $object_id_key = $form[$field_name][$langcode][$delta]['#object_id_key'];
+    $subject_id_key = $this->subject_id_column;
+    $object_id_key = $this->object_id_column;
 
     // Retrieve the values from the form for the current $delta.
     $voc_id = array_key_exists('vocabulary', $form_state['values'][$field_name][$langcode][$delta]) ? $form_state['values'][$field_name][$langcode][$delta]['vocabulary'] : '';
@@ -419,86 +343,128 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     $subject_name = $form_state['values'][$field_name][$langcode][$delta]['subject_name'];
     $object_name = $form_state['values'][$field_name][$langcode][$delta]['object_name'];
 
+    // Validation:
+    //------------
     // If the row is empty then skip this one, there's nothing to validate.
-    if (!($type_id or !$type_name) and !$subject_name and !$object_name) {
+    if (!($type_id OR $type_name) and !$subject_name and !$object_name) {
       return;
     }
-    else if ($type_name && $voc_id) {
-      $val = array(
-        'cv_id' => $voc_id,
-        'name' => $type_name
-      );
-      $cvterm = chado_generate_var('cvterm', $val);
-      $type_id = $cvterm->cvterm_id;
-    }
 
     // Do not proceed if subject ID or object ID does not exist
     if (!key_exists($subject_id_key, $fkeys[$base_table]['columns']) ||
         !key_exists($object_id_key, $fkeys[$base_table]['columns'])) {
       return;
     }
-    // Get the subject ID.
-    $subject_id = '';
-    $fkey_rcolumn = $fkeys[$base_table]['columns'][$subject_id_key];
-    $matches = array();
-    // First check if it's in the textfield due to use of the autocomplete.
-    if (preg_match('/\[id: (\d+)\]/', $subject_name, $matches)) {
-      $subject_id =  $matches[1];
-    }
-    // Otherwise, try the uniquename.
-    // The base table does not always have a uniquename (e.g. organism for organism_relationship).
-    // @uniquename this will cause an error on organism/project-based content types.
-    else {
-      $values = array('uniquename' => $subject_name);
-      $subject = chado_select_record($base_table, array($fkey_rcolumn), $values);
-      if(count($subject) > 0) {
-        $subject_id = $subject[0]->$fkey_rcolumn;
+
+    // Validation is occuring in the field::validate() but we need to know if it finds errors.
+    // As such, I'm calling it here to check.
+    // Also, field::validate() doesn't seem to always show it's errors
+    // OR stop form submission? so we need to ensure that happens here.
+    // sbo__relationship::validate($entity_type, $entity, $langcode, $items, &$errors)
+    $errors = $this->field_instance->validateItem($form_state['values'][$field_name][$langcode][$delta], $element['#chado_record_id']);
+    if ($errors) {
+      foreach ($errors as $error) {
+        switch ($error['element']) {
+          case 'subject':
+            form_set_error('sbo__relationship]['.$langcode.']['.$delta.'][subject_name', $error['message']);
+            break;
+          case 'type':
+            if (isset($element['vocabulary'])) {
+              form_set_error('sbo__relationship]['.$langcode.']['.$delta.'][type_name', $error['message']);
+              form_set_error('sbo__relationship]['.$langcode.']['.$delta.'][vocabulary', '');
+            }
+            elseif (isset($element['type_id'])) {
+              form_set_error('sbo__relationship]['.$langcode.']['.$delta.'][type_id', $error['message']);
+            }
+            break;
+          case 'object':
+            form_set_error('sbo__relationship]['.$langcode.']['.$delta.'][object_name', $error['message']);
+            break;
+          default:
+            form_set_error('sbo__relationship]['.$langcode.']['.$delta, $error['message']);
+        }
       }
-    }
 
-    // Get the object ID.
-    $object_id = '';
-    $fkey_rcolumn = $fkeys[$base_table]['columns'][$object_id_key];
-    $matches = array();
-    // First check if it's in the textfield due to use of the autocomplete.
-    if (preg_match('/\[id: (\d+)\]/', $object_name, $matches)) {
-      $object_id = $matches[1];
+    // Ensure data is prepared for the storage backend:
+    //-------------------------------------------------
     }
-    // Otherwise, try the uniquename.
-    // The base table does not always have a uniquename (e.g. organism for organism_relationship).
-    // @uniquename this will cause an error on organism/project-based content types.
     else {
-      $values = array('uniquename' => $object_name);
-      $object = chado_select_record($base_table, array($fkey_rcolumn), $values);
-      if (count($object) > 0) {
-        $object_id = $object[0]->$fkey_rcolumn;
+
+      if ($type_name && $voc_id) {
+        $val = array(
+          'cv_id' => $voc_id,
+          'name' => $type_name
+        );
+        $cvterm = chado_generate_var('cvterm', $val);
+
+        if (isset($cvterm->cvterm_id)) {
+          $type_id = $cvterm->cvterm_id;
+        }
       }
-    }
 
-    // If we have all three values required for a relationship...
-    // Then set them as the chado field storage expects them to be set.
-    if ($subject_id && $object_id && $type_id) {
-      // Set the IDs according to the values that were determined above.
-      $form_state['values'][$field_name][$langcode][$delta]['value'] = 'value must be set but is not used';
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $subject_id_key] = $subject_id;
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $object_id_key] = $object_id;
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__type_id'] = $type_id;
-      if (array_key_exists('rank', $this->schema['fields'])) {
-        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__rank'] = $form_state['values'][$field_name][$langcode][$delta]['_weight'];
+      // Get the subject ID.
+      $subject_id = '';
+      $fkey_rcolumn = $fkeys[$base_table]['columns'][$subject_id_key];
+      $matches = array();
+      // First check if it's in the textfield due to use of the autocomplete.
+      if (preg_match('/\[id: (\d+)\]/', $subject_name, $matches)) {
+        $subject_id =  $matches[1];
       }
-    }
-    // Otherwise, leave them blank?
-    else {
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $subject_id_key] = '';
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $object_id_key] = '';
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__type_id'] = '';
-      $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__value'] = '';
-      if (array_key_exists('rank', $this->schema['fields'])) {
-        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__rank'] = '';
+      // Otherwise we need to look it up using the name field determined in the
+      // constructor for the current field. There may be more then one name field
+      // (e.g. organism: genus + species) so we want to check both.
+      else {
+        $sql = 'SELECT ' . $fkey_rcolumn . ' FROM {' . $base_table . '} WHERE ' . implode('||', $this->base_name_columns) . '=:keyword';
+        $subject = chado_query($sql, [':keyword' => $subject_name])->fetchAll();
+        if(count($subject) > 0) {
+          $subject_id = $subject[0]->$fkey_rcolumn;
+        }
+      }
+
+      // Get the object ID.
+      $object_id = '';
+      $fkey_rcolumn = $fkeys[$base_table]['columns'][$object_id_key];
+      $matches = array();
+      // First check if it's in the textfield due to use of the autocomplete.
+      if (preg_match('/\[id: (\d+)\]/', $object_name, $matches)) {
+        $object_id = $matches[1];
+      }
+      // Otherwise we need to look it up using the name field determined in the
+      // constructor for the current field. There may be more then one name field
+      // (e.g. organism: genus + species) so we want to check both.
+      else {
+        $sql = 'SELECT ' . $fkey_rcolumn . ' FROM {' . $base_table . '} WHERE ' . implode('||', $this->base_name_columns) . '=:keyword';
+        $object = chado_query($sql, [':keyword' => $object_name])->fetchAll();
+        if (count($object) > 0) {
+          $object_id = $object[0]->$fkey_rcolumn;
+        }
       }
-    }
 
-    // @debug ddl($form_state['values'][$field_name][$langcode][$delta], "form state values: $delta");
+      // If we have all three values required for a relationship...
+      // Then set them as the chado field storage expects them to be set.
+      if ($subject_id && $object_id && $type_id) {
+        // Set the IDs according to the values that were determined above.
+        $form_state['values'][$field_name][$langcode][$delta]['value'] = 'value must be set but is not used';
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $subject_id_key] = $subject_id;
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $object_id_key] = $object_id;
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__type_id'] = $type_id;
+        if (array_key_exists('rank', $this->schema['fields'])) {
+          $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__rank'] = $form_state['values'][$field_name][$langcode][$delta]['_weight'];
+        }
+      }
+      // Otherwise, leave them blank?
+      else {
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $subject_id_key] = '';
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $object_id_key] = '';
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__type_id'] = '';
+        $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__value'] = '';
+        if (array_key_exists('rank', $this->schema['fields'])) {
+          $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__rank'] = '';
+        }
+      }
+    }
+    // @debug
+    dpm($form_state['values'][$field_name][$langcode][$delta], "form state values: $delta");
   }
 
   /**
@@ -527,6 +493,112 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     return $layout;
   }
 
+  /**
+   * Retrieve options for the type drop-down for the relationship widget.
+   */
+  public function get_rtype_select_options() {
+
+    // Get the instance settings. There are three options for how this widget
+    // will be displayed. Those are controlled in the instance settings
+    // of the field.
+    // Option 1:  relationship types are limited to a specific vocabulary.
+    // Option 2:  relationship types are limited to a subset of one vocabulary.
+    // Option 3:  relationship types are limited to a predefined set.
+    $instance = $this->instance;
+    $settings = '';
+    $option1_vocabs = '';
+    $option2_parent = '';
+    $option2_vocab = '';
+    $option3_rtypes  = '';
+    if (array_key_exists('relationships', $instance)) {
+      $settings = $instance['settings']['relationships'];
+      $option1_vocabs = $settings['option1_vocabs'];
+      $option2_vocab  = $settings['option2_vocab'];
+      $option2_parent = $settings['option2_parent'];
+      $option3_rtypes = $settings['relationship_types'];
+    }
+
+    // For testing if there are selected vocabs for option1 we'll copy the
+    // contents in a special variable for later.
+    $option1_test = $option1_vocabs;
+
+    // Option 3: Custom list of Relationship Types
+    $rtype_options = array();
+    if ($option3_rtypes) {
+      $rtypes = explode(PHP_EOL, $option3_rtypes);
+      foreach($rtypes AS $rtype) {
+        // Ignore empty lines
+        if (trim($rtype) == '') {
+          continue;
+        }
+        $term = chado_get_cvterm(array('name' => trim($rtype)));
+        // Try to get term with vocabulary specified
+        if (!$term) {
+          $tmp = explode('|', trim($rtype), 2);
+          $cv = chado_get_cv(array('name' => trim($tmp[0])));
+          $rtype = trim($tmp[1]);
+          $term = chado_get_cvterm(array('name' => $rtype, 'cv_id' => $cv->cv_id));
+        }
+        $rtype_options[$term->cvterm_id] = $term->name;
+      }
+      return $rtype_options;
+    }
+    // Option 2: Child terms of a selected cvterm
+    else if ($option2_vocab) {
+      $values = array(
+        'cv_id' => $option2_vocab,
+        'name' => $option2_parent
+      );
+      $parent_term = chado_get_cvterm($values);
+
+      // If the term wasn't found then see if it's a synonym.
+      if(!$parent_term) {
+        $values = array(
+          'synonym' => array(
+            'name' => trim($option2_parent),
+          )
+        );
+        $synonym = chado_get_cvterm($values);
+        if ($synonym && $synonym->cv_id->cv_id == $option2_vocab) {
+          $parent_term = $synonym;
+        }
+      }
+      // Get the child terms of the parent term found above.
+      $sql = "
+        SELECT subject_id,
+          (SELECT name from {cvterm} where cvterm_id = subject_id) AS name
+        FROM {cvtermpath}
+        WHERE
+          object_id = :parent_cvterm_id AND
+          cv_id = :parent_cv_id
+        ORDER BY name
+       ";
+      $args = array(
+        ':parent_cvterm_id' => $parent_term->cvterm_id,
+        ':parent_cv_id' => $parent_term->cv_id->cv_id
+      );
+      $results = chado_query($sql, $args);
+      while($child = $results->fetchObject()) {
+        $rtype_options[$child->subject_id] = $child->name;
+      }
+      return $rtype_options;
+    }
+    // Option 1: All terms of selected vocabularies
+    else if ($option1_test && array_pop($option1_test)) {
+      $sql = "SELECT cvterm_id, name FROM {cvterm} WHERE cv_id IN (:cv_id) ORDER BY name";
+      $results = chado_query($sql, array(':cv_id' => $option1_vocabs));
+      while ($obj = $results->fetchObject()) {
+        $rtype_options[$obj->cvterm_id] = $obj->name;
+      }
+      return $rtype_options;
+    }
+    // Default option:
+    // Let the form deal with this by providing a type autocomplete?
+    else {
+      return FALSE;
+    }
+  }
+
 }
 
 function theme_sbo__relationship_instance_settings ($variables) {