Просмотр исходного кода

Merge pull request #726 from tripal/531-t3-relationship_widget

Relationship Widget
Stephen Ficklin 6 лет назад
Родитель
Сommit
be494de1cb

+ 18 - 0
tests/DataFactory.php

@@ -73,6 +73,24 @@ Factory::define('chado.feature', function (Faker\Generator $faker) {
 });
 
 
+/** @see  StatonLab\TripalTestSuite\Database\Factory::define() */
+Factory::define('chado.stock', function (Faker\Generator $faker) {
+    return [
+        'name' => $faker->name,
+        'uniquename' => $faker->unique()->name,
+        'organism_id' => factory('chado.organism')->create()->organism_id,
+        'type_id' => factory('chado.cvterm')->create()->cvterm_id,
+    ];
+});
+
+/** @see  StatonLab\TripalTestSuite\Database\Factory::define() */
+Factory::define('chado.project', function (Faker\Generator $faker) {
+    return [
+        'name' => $faker->name,
+    ];
+});
+
+
 Factory::define('chado.analysis', function (Faker\Generator $faker) {
   return [
     'name' => $faker->name,

+ 236 - 0
tests/TripalFieldTestHelper.php

@@ -0,0 +1,236 @@
+<?php
+/**
+ * This class can be included at the top of your TripalTestCase to facillitate testing
+ * fields, widgets and formatters.
+ */
+class TripalFieldTestHelper {
+
+  // This is for the initialized field, widget or formatter as indicated by
+  // $machine_names in the constructor.
+  public $initialized_class;
+
+  // The name of the class being initialized.
+  public $class_name;
+
+  // Information for the field.
+  public $field_info;
+
+  // Information for the field instance.
+  public $instance_info;
+
+  // The loaded bundle the field is attached to.
+  public $bundle;
+
+  // The entity the field is attached to.
+  public $entity;
+
+  // The type of class we are initializing.
+  // One of 'field', 'widget', or 'formatter'.
+  public $type;
+
+  // The name of the field for the class being tested.
+  public $field_name;
+
+  /**
+   * Create an instance of TripalFieldTestHelper.
+   *
+   * Specifcally, initialize the widget class and save it for further testing.
+   *
+   * @param $bundle_name
+   *   The name of the bundle the field should be attached to. This bundle must already exist.
+   * @param $machine_names
+   *   An array of machine names including:
+   *    - field_name: the name of the field (REQUIRED)
+   *    - widget_name: the name of the widget (Only required for widget testing)
+   *    - formatter_name: the name of the formatter (Only required for formatter testing)
+   * @param $field_info
+   *   The Drupal information for the field you want to test.
+   * @param $instance_info
+   *   The Drupal information for the field instance you want to test.
+   */
+  public function __construct($bundle_name, $machine_names, $entity, $field_info, $instance_info) {
+
+
+    // What type of class are we initializing?
+    $this->type = 'field';
+    $this->class_name = $machine_names['field_name'];
+    if (isset($machine_names['widget_name'])) {
+      $this->type = 'widget';
+      $this->class_name = $machine_names['widget_name'];
+    }
+    elseif (isset($machine_names['formatter_name'])) {
+      $this->type = 'formatter';
+      $this->class_name = $machine_names['formatter_name'];
+    }
+    $this->field_name = $machine_names['field_name'];
+
+    $class_name = '\\' . $this->class_name;
+    $class_path = DRUPAL_ROOT . '/' . drupal_get_path('module', 'tripal_chado')
+      . '/includes/TripalFields/'.$machine_names['field_name'].'/'.$this->class_name.'.inc';
+    if ((include_once($class_path)) == TRUE) {
+
+      // Save the field information.
+      if (!$field_info) {
+        $field_info = $this->getFieldInfo($machine_names['field_name']);
+      }
+      $this->field_info = $field_info;
+
+      // Save the field instance information.
+      if (!$instance_info) {
+        $instance_info = $this->getFieldInfo($bundle_name, $machine_names['field_name']);
+      }
+      $this->instance_info = $instance_info;
+
+      // Load the bundle.
+      $this->bundle = tripal_load_bundle_entity(array('name'=> $bundle_name));
+
+      // The entity from the specified bundle that the field should be attached to.
+      $this->entity = $entity;
+
+
+      // Initialize the class.
+      $this->initialized_class = new $class_name($this->field_info, $this->instance_info);
+    }
+
+  }
+
+  /**
+   * Retrieve the initialized class for testing!
+   */
+  public function getInitializedClass() {
+    return $this->initialized_class;
+  }
+
+  /**
+   * Retrieve the field information for a given field.
+   * @see https://api.drupal.org/api/drupal/modules%21field%21field.info.inc/function/field_info_field/7.x
+   *
+   * @param $field_name
+   *   The name of the field to retrieve. $field_name can only refer to a
+   *   non-deleted, active field.
+   * @return
+   *   The field array as returned by field_info_field() and used when initializing
+   *   this class.
+   */
+  public function getFieldInfo($field_name) {
+
+    if (empty($this->field_info)) {
+      $this->field_info = field_info_field($field_name);
+    }
+
+    return $this->field_info;
+  }
+
+  /**
+   * Retrieve the field instance information for a given field.
+   * @see https://api.drupal.org/api/drupal/modules%21field%21field.info.inc/function/field_info_instance/7.x
+   *
+   * @param $bundle_name
+   *   The name of the bundle you want the field attached to. For example, bio_data_1.
+   * @param $field_name
+   *   The name of the field to retrieve the instance of. $field_name can only refer to a
+   *   non-deleted, active field.
+   * @return
+   *   The field instance array as returned by field_info_instance() and used when
+   *   initializing this class.
+   */
+  public function getInstanceInfo($bundle_name, $field_name) {
+
+    if (empty($this->instance_info)) {
+      $this->instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    }
+
+    return $this->instance_info;
+  }
+
+  /**
+   * Create a fake version of the $element parameter used in many field methods
+   * (e.g. TripalFieldWidget::form).
+   *
+   * @param $delta
+   *   The delta for the $element you want to fake.
+   * @param $langcode
+   *   The language code for the field/widget. This should usually be LANGUAGE_NONE.
+   * @param $required
+   *   True if the widget is required and false otherwise.
+   *
+   * @return
+   *   A fake $element variable for use in testing.
+   */
+  public function mockElement($delta = 0, $langcode = LANGUAGE_NONE, $required = FALSE) {
+    return [
+      '#entity_type' => 'TripalEntity',
+      '#entity' => $this->entity,
+      '#bundle' => $this->bundle,
+      '#field_name' => $this->field_name,
+      '#language' => $langcode,
+      '#field_parents' => [],
+      '#columns' => [],
+      '#title' => '',
+      '#description' => '',
+      '#required' => $required,
+      '#delta' => $delta,
+      '#weight' => $delta, //same as delta.
+      'value' => [
+        '#type' => 'value',
+        '#value' => '',
+      ],
+      '#field' => $this->field_info,
+      '#instance' => $this->instance_info,
+      '#theme' => 'tripal_field_default',
+      'element_validate' => ['tripal_field_widget_form_validate']
+    ];
+  }
+
+  /**
+   * Create a fake version of the create/edit content form with the
+   * current entity attached.
+   *
+   * @return
+   *   A fake $form array for use in testing.
+   */
+  public function mockForm() {
+    return [
+      '#parents' => [],
+      '#entity' => $this->entity,
+    ];
+  }
+
+  /**
+   * Create a fake version of the create/edit content form_state
+   * with the current entity attached.
+   *
+   * @param $delta
+   *   The delta for the $element you want to fake.
+   * @param $langcode
+   *   The language code for the field/widget. This should usually be LANGUAGE_NONE.
+   * @param $values
+   *    An array of values where the key is the form element name and the value is
+   *    the fake user submmitted value.
+   * @return
+   *   A fake $form_state array for use in testing.
+   */
+  public function mockFormState($delta = 0, $langcode = LANGUAGE_NONE, $values = NULL) {
+    $form_state = [
+      'build_info' => [
+        'args' => [
+          0 => NULL,
+          1 => $entity,
+        ],
+        'form_id' => 'tripal_entity_form',
+      ],
+      'rebuild' => FALSE,
+      'rebuild_info' => [],
+      'redirect' => NULL,
+      'temporary' => [],
+      'submitted' => FALSE,
+    ];
+
+    if ($values !== NULL) {
+      $form_state['values'][$this->field_name][$langcode][$delta] = $values;
+    }
+
+    return $form_state;
+  }
+
+}

+ 614 - 0
tests/tripal_chado/fields/sbo__relationship_widgetTest.php

@@ -0,0 +1,614 @@
+<?php
+namespace Tests\tripal_chado\fields;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+
+module_load_include('php', 'tripal_chado', '../tests/TripalFieldTestHelper');
+
+class sbo__relationship_widgetTest extends TripalTestCase {
+  // Uncomment to auto start and rollback db transactions per test method.
+  use DBTransaction;
+
+  /**
+   * Data Provider: provides entities matching important test cases.
+   *
+   * Specifically, we will cover three relationship tables, which represent
+   * the diversity in the chado schema v1.3:
+   *  organism_relationship: subject_id, type_id, object_id,
+   *  stock_relationship: subject_id, type_id, object_id, value, rank,
+   *  project_relationship: subject_project_id, type_id, object_project_id, rank
+   *
+   * @returns
+   *   Returns an array where each item to be tested has the paramaters
+   *   needed for initializeWidgetClass(). Specfically, $bundle_name,
+   *   $field_name, $widget_name, $entity_ids, $expect.
+   */
+  public function provideEntities() {
+     $data = [];
+
+     foreach (['organism', 'stock', 'project'] as $base_table) {
+
+       $field_name = 'sbo__relationship';
+       $widget_name = 'sbo__relationship_widget';
+
+       // Find a bundle which stores it's data in the given base table.
+       // This will work on Travis since Tripal creates matching bundles by default.
+       $bundle_details = db_query("
+         SELECT bundle_id, type_column, type_id
+         FROM chado_bundle b
+         WHERE data_table=:table AND type_linker_table=''
+         ORDER BY bundle_id ASC LIMIT 1",
+           array(':table' => $base_table))->fetchObject();
+       if (isset($bundle_details->bundle_id)) {
+         $bundle_id = $bundle_details->bundle_id;
+       }
+       else {
+         continue;
+       }
+
+       $bundle_name = 'bio_data_'.$bundle_id;
+
+       // Create some entities so that we know there are some available to find.
+       if ($bundle_details->type_column == 'type_id') {
+         $chado_records = factory('chado.'. $base_table, 2)->create(['type_id' => $bundle_details->type_id]);
+       }
+       else {
+         $chado_records = factory('chado.'. $base_table, 2)->create();
+       }
+       // Then publish them so we have entities.
+       $this->publish($base_table);
+
+       // Find our fake entities from the above bundle.
+       $entity_ids = [];
+       $entity_ids[] = db_query('SELECT entity_id FROM chado_'.$bundle_name.' WHERE record_id=:chado_id',
+         array(':chado_id' => $chado_records[0]->{$base_table.'_id'}))->fetchField();
+       $entity_ids[] = db_query('SELECT entity_id FROM chado_'.$bundle_name.' WHERE record_id=:chado_id',
+         array(':chado_id' => $chado_records[1]->{$base_table.'_id'}))->fetchField();
+
+       // set variables to guide testing.
+       $expect = [
+         'has_rank' => TRUE,
+         'has_value' => FALSE,
+         'subject_key' => 'subject_id',
+         'object_key' => 'object_id',
+         'base_table' => $base_table,
+         'relationship_table' => $base_table.'_relationship'
+       ];
+       if ($base_table == 'organism') { $expect['has_rank'] = FALSE; }
+       if ($base_table == 'stock') { $expect['has_value'] = TRUE; }
+       if ($base_table == 'project') {
+         $expect['subject_key'] = 'subject_project_id';
+         $expect['object_key'] = 'object_project_id';
+       }
+
+       $data[] = [$bundle_name, $field_name, $widget_name, $entity_ids, $expect];
+     }
+     return $data;
+  }
+
+  /**
+   * Test that we can initialize the widget properly.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetClassInitialization($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+    $entity_id = $entity_ids[0];
+
+    // Load the entity.
+    $entity = entity_load('TripalEntity', [$entity_id]);
+    $entity = $entity[$entity_id];
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    // Check we have the variables we initialized.
+    $this->assertNotEmpty($helper->bundle,
+      "Could not load the bundle.");
+    $this->assertNotEmpty($helper->getFieldInfo($field_name),
+      "Could not lookup the field information.");
+    $this->assertNotEmpty($helper->getInstanceInfo($bundle_name, $field_name),
+      "Could not lookup the instance information.");
+    $this->assertNotEmpty($widget_class,
+      "Couldn't create a widget class instance.");
+    $this->assertNotEmpty($entity,
+      "Couldn't load an entity.");
+
+    // Check a little deeper...
+    $this->assertEquals($helper->instance_info['settings']['chado_table'], $expect['relationship_table'],
+      "Instance settings were not initialized fully.");
+
+  }
+
+  /**
+   * Test the widget Form.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetForm($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+    $entity_id = $entity_ids[0];
+
+    // Load the entity.
+    $entity = entity_load('TripalEntity', [$entity_ids]);
+    $entity = $entity[$entity_id];
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    $base_table = $entity->chado_table;
+
+    // Stub out a fake objects.
+    $delta = 1;
+    $langcode = LANGUAGE_NONE;
+    $widget = $helper->mockElement($delta, $langcode);
+    $form = $helper->mockForm($delta, $langcode);
+    $form_state = $helper->mockFormState($delta, $langcode);
+    $element = $helper->mockElement($delta, $langcode);
+
+    $items = [
+      'value' => '',
+      'chado-'.$base_table.'_relationship__organism_relationship_id' => '',
+      'chado-'.$base_table.'_relationship__subject_id' => '',
+      'chado-'.$base_table.'_relationship__object_id' => '',
+      'chado-'.$base_table.'_relationship__type_id' => '',
+      'object_name' => '',
+      'subject_name' => '',
+      'type_name' => '',
+    ];
+
+    // Execute the form method.
+    $widget_class->form($widget, $form, $form_state, $langcode, $items, $delta, $element);
+
+    // Check the resulting for array
+    $this->assertArrayHasKey('subject_name', $widget,
+      "The form for $bundle_name($base_table) does not have a subject element.");
+    $this->assertArrayHasKey('type_name', $widget,
+      "The form for $bundle_name($base_table) does not have a type element.");
+    $this->assertArrayHasKey('object_name', $widget,
+      "The form for $bundle_name($base_table) does not have a object element.");
+
+    // Check the subject/object keys were correctly determined.
+    $this->assertEquals($expect['subject_key'], $widget['#subject_id_key'],
+      "The form didn't determine the subject key correctly.");
+    $this->assertEquals($expect['object_key'], $widget['#object_id_key'],
+      "The form didn't determine the object key correctly.");
+
+  }
+
+  /**
+   * Case: WidgetValidate on existing relationship.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetValidate_existing($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+
+    // Load the entities.
+    $entities = entity_load('TripalEntity', $entity_ids);
+    $entity1 = $entities[$entity_ids[0]];
+    $entity2 = $entities[$entity_ids[1]];
+    $base_table = $entity1->chado_table;
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity1, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    // Set some initial values.
+    $cvterm = factory('chado.cvterm')->create();
+    $initial_values = [
+      'subject_name' => $entity2->chado_record->name,
+      'type_name' => $cvterm->name,
+      'vocabulary' => $cvterm->cv_id,
+      'object_name' => $entity1->chado_record->name,
+      // Both the form and load set the chado values
+      // so we will set them here as well.
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'] => $entity2->chado_record->{$base_table.'_id'},
+      'chado-'.$base_table.'_relationship__type_id' => $cvterm->cvterm_id,
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'] => $entity1->chado_record->{$base_table.'_id'},
+    ];
+    if ($base_table == 'organism') {
+      $initial_values['subject_name'] = $entity2->chado_record->species;
+      $initial_values['object_name'] = $entity1->chado_record->species;
+    }
+
+    // Mock objects.
+    $delta = 1;
+    $langcode = LANGUAGE_NONE;
+    $widget = $helper->mockElement($delta, $langcode);
+    $form = $helper->mockForm($delta, $langcode);
+    $form_state = $helper->mockFormState($delta, $langcode, $initial_values);
+    $element = $helper->mockElement($delta, $langcode);
+
+    $widget_class->validate($element, $form, $form_state, $langcode, $delta);
+
+    // @debug print_r($form_state['values'][$field_name][$langcode][$delta]);
+
+    // Ensure the chado-table__column entries are there.
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the subject_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the subject.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the object_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the object.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__type_id',
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the type_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the type.'
+    );
+
+    // Check for errors.
+    $errors = form_get_errors();
+    // @debug print "Errors: " . print_r($errors, TRUE)."\n";
+
+    $this->assertEmpty($errors,
+      "There should be no form errors when subject and object are pre-existing and both are supplied. Initial values: ".print_r($initial_values,TRUE)." But these were registered: ".print_r($errors, TRUE));
+
+    // Clean up after ourselves by removing any errors we logged.
+    form_clear_error();
+  }
+
+  /**
+   * Case: WidgetValidate on new relationship filled out properly.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetValidate_create($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+
+    // Load the entities.
+    $entities = entity_load('TripalEntity', $entity_ids);
+    $entity1 = $entities[$entity_ids[0]];
+    $entity2 = $entities[$entity_ids[1]];
+    $base_table = $entity1->chado_table;
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity1, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    // Set some initial values.
+    $cvterm = factory('chado.cvterm')->create();
+    $initial_values = [
+      'subject_name' => $entity2->chado_record->name,
+      'type_name' => $cvterm->name,
+      'vocabulary' => $cvterm->cv_id,
+      'object_name' => $entity1->chado_record->name,
+      // These are not set on the creation form.
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'] => NULL,
+      'chado-'.$base_table.'_relationship__type_id' => NULL,
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'] => NULL,
+    ];
+    if ($base_table == 'organism') {
+      $initial_values['subject_name'] = $entity2->chado_record->species;
+      $initial_values['object_name'] = $entity1->chado_record->species;
+    }
+
+    // Mock objects.
+    $delta = 1;
+    $langcode = LANGUAGE_NONE;
+    $widget = $helper->mockElement($delta, $langcode);
+    $form = $helper->mockForm($delta, $langcode);
+    $form_state = $helper->mockFormState($delta, $langcode, $initial_values);
+    $element = $helper->mockElement($delta, $langcode);
+
+    $widget_class->validate($element, $form, $form_state, $langcode, $delta);
+
+    // @debug print_r($form_state['values'][$field_name][$langcode][$delta]);
+
+    // Ensure the chado-table__column entries are there.
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the subject_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the subject.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the object_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the object.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__type_id',
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the type_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the type.'
+    );
+
+    // Check for errors.
+    $errors = form_get_errors();
+    // @debug print "Errors: " . print_r($errors, TRUE)."\n";
+
+    $this->assertEmpty($errors,
+      "There should be no form errors when subject and object are pre-existing and both are supplied. Initial values: ".print_r($initial_values,TRUE)." But these were registered: ".print_r($errors, TRUE));
+
+    // Clean up after ourselves by removing any errors we logged.
+    form_clear_error();
+  }
+
+  /**
+   * Case: WidgetValidate on new relationship missing subject.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetValidate_nosubject($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+
+    // Load the entities.
+    $entities = entity_load('TripalEntity', $entity_ids);
+    $entity1 = $entities[$entity_ids[0]];
+    $entity2 = $entities[$entity_ids[1]];
+    $base_table = $entity1->chado_table;
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity1, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    // Set some initial values.
+    $cvterm = factory('chado.cvterm')->create();
+    $initial_values = [
+      'subject_name' => '',
+      'type_name' => $cvterm->name,
+      'vocabulary' => $cvterm->cv_id,
+      'object_name' => $entity1->chado_record->name,
+      // Both the form and load set the chado values
+      // so we will set them here as well.
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'] => NULL,
+      'chado-'.$base_table.'_relationship__type_id' => $cvterm->cvterm_id,
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'] => $entity1->chado_record->{$base_table.'_id'},
+    ];
+    if ($base_table == 'organism') {
+      $initial_values['object_name'] = $entity1->chado_record->species;
+    }
+
+    // Mock objects.
+    $delta = 1;
+    $langcode = LANGUAGE_NONE;
+    $widget = $helper->mockElement($delta, $langcode);
+    $form = $helper->mockForm($delta, $langcode);
+    $form_state = $helper->mockFormState($delta, $langcode, $initial_values);
+    $element = $helper->mockElement($delta, $langcode);
+
+    $widget_class->validate($element, $form, $form_state, $langcode, $delta);
+
+    // @debug print_r($form_state['values'][$field_name][$langcode][$delta]);
+
+    // Ensure the chado-table__column entries are there.
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the subject_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the subject.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the object_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the object.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__type_id',
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the type_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the type.'
+    );
+
+    // Check for errors.
+    $errors = form_get_errors();
+    // @debug print "Errors: " . print_r($errors, TRUE)."\n";
+
+    $this->assertNotEmpty($errors,
+      "There should be form errors when subject is not supplied. Initial values: ".print_r($initial_values,TRUE)." But these were registered: ".print_r($errors, TRUE));
+
+    // Clean up after ourselves by removing any errors we logged.
+    form_clear_error();
+  }
+
+  /**
+   * Case: WidgetValidate on new relationship missing object.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetValidate_noobject($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+
+    // Load the entities.
+    $entities = entity_load('TripalEntity', $entity_ids);
+    $entity1 = $entities[$entity_ids[0]];
+    $entity2 = $entities[$entity_ids[1]];
+    $base_table = $entity1->chado_table;
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity1, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    // Set some initial values.
+    $cvterm = factory('chado.cvterm')->create();
+    $initial_values = [
+      'subject_name' => $entity2->chado_record->name,
+      'type_name' => $cvterm->name,
+      'vocabulary' => $cvterm->cv_id,
+      'object_name' => '',
+      // Both the form and load set the chado values
+      // so we will set them here as well.
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'] => $entity2->chado_record->{$base_table.'_id'},
+      'chado-'.$base_table.'_relationship__type_id' => $cvterm->cvterm_id,
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'] => NULL,
+    ];
+    if ($base_table == 'organism') {
+      $initial_values['subject_name'] = $entity2->chado_record->species;
+    }
+
+    // Mock objects.
+    $delta = 1;
+    $langcode = LANGUAGE_NONE;
+    $widget = $helper->mockElement($delta, $langcode);
+    $form = $helper->mockForm($delta, $langcode);
+    $form_state = $helper->mockFormState($delta, $langcode, $initial_values);
+    $element = $helper->mockElement($delta, $langcode);
+
+    $widget_class->validate($element, $form, $form_state, $langcode, $delta);
+
+    // @debug print_r($form_state['values'][$field_name][$langcode][$delta]);
+
+    // Ensure the chado-table__column entries are there.
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the subject_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the subject.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the object_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the object.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__type_id',
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the type_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the type.'
+    );
+
+    // Check for errors.
+    $errors = form_get_errors();
+    // @debug print "Errors: " . print_r($errors, TRUE)."\n";
+
+    $this->assertNotEmpty($errors,
+      "There should be form errors when the object is not supplied. Initial values: ".print_r($initial_values,TRUE)." But these were registered: ".print_r($errors, TRUE));
+
+    // Clean up after ourselves by removing any errors we logged.
+    form_clear_error();
+  }
+
+  /**
+   * Case: WidgetValidate on new relationship missing type.
+   *
+   * @dataProvider provideEntities()
+   *
+   * @group widget
+   * @group sbo__relationship
+   */
+  public function testWidgetValidate_notype($bundle_name, $field_name, $widget_name, $entity_ids, $expect) {
+
+    // Load the entities.
+    $entities = entity_load('TripalEntity', $entity_ids);
+    $entity1 = $entities[$entity_ids[0]];
+    $entity2 = $entities[$entity_ids[1]];
+    $base_table = $entity1->chado_table;
+
+    // Initialize the widget class via the TripalFieldTestHelper class.
+    $machine_names = array(
+      'field_name' => $field_name,
+      'widget_name' => $widget_name,
+    );
+    $field_info = field_info_field($field_name);
+    $instance_info = field_info_instance('TripalEntity', $field_name, $bundle_name);
+    $helper = new \TripalFieldTestHelper($bundle_name, $machine_names, $entity1, $field_info, $instance_info);
+    $widget_class = $helper->getInitializedClass();
+
+    // Set some initial values.
+    $initial_values = [
+      'subject_name' => $entity2->chado_record->name,
+      'type_name' => '',
+      'vocabulary' => NULL,
+      'object_name' => $entity1->chado_record->name,
+      // Both the form and load set the chado values
+      // so we will set them here as well.
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'] => $entity2->chado_record->{$base_table.'_id'},
+      'chado-'.$base_table.'_relationship__type_id' => NULL,
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'] => $entity1->chado_record->{$base_table.'_id'},
+    ];
+    if ($base_table == 'organism') {
+      $initial_values['subject_name'] = $entity2->chado_record->species;
+      $initial_values['object_name'] = $entity1->chado_record->species;
+    }
+
+    // Mock objects.
+    $delta = 1;
+    $langcode = LANGUAGE_NONE;
+    $widget = $helper->mockElement($delta, $langcode);
+    $form = $helper->mockForm($delta, $langcode);
+    $form_state = $helper->mockFormState($delta, $langcode, $initial_values);
+    $element = $helper->mockElement($delta, $langcode);
+
+    $widget_class->validate($element, $form, $form_state, $langcode, $delta);
+
+    // @debug print_r($form_state['values'][$field_name][$langcode][$delta]);
+
+    // Ensure the chado-table__column entries are there.
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['subject_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the subject_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the subject.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__'.$expect['object_key'],
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the object_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the object.'
+    );
+    $this->assertArrayHasKey(
+      'chado-'.$base_table.'_relationship__type_id',
+      $form_state['values'][$field_name][$langcode][$delta],
+      'Failed to find the type_id in the processed values (Base: '.$base_table.'). This implies the validate function was not able to validate the type.'
+    );
+
+    // Check for errors.
+    $errors = form_get_errors();
+    // @debug print "Errors: " . print_r($errors, TRUE)."\n";
+
+    $this->assertNotEmpty($errors,
+      "There should be form errors when type is not supplied. Initial values: ".print_r($initial_values,TRUE)." But these were registered: ".print_r($errors, TRUE));
+
+    // Clean up after ourselves by removing any errors we logged.
+    form_clear_error();
+  }
+
+}

+ 48 - 0
tripal_chado/api/modules/tripal_chado.project.api.inc

@@ -0,0 +1,48 @@
+<?php
+/**
+ * @file
+ * Provides API functions specificially for managing project
+ * records in Chado.
+ */
+
+/**
+ * @defgroup tripal_project_api Chado Project
+ * @ingroup tripal_chado_api
+ * @{
+ * Provides API functions specificially for managing project
+ * records in Chado.  The project table of Chado is used for storing a variety
+ * of data types besides just projects from a project collection.  Examples of
+ * other records commonly stored in the project table are germplasm and
+ * individuals from a breeding population.
+ * @}
+ */
+
+/**
+ * Used for autocomplete in forms for identifying projects 
+ *
+ * @param $string
+ *   The string to search for.
+ *    
+ * @return
+ *   A json array of terms that begin with the provided string.
+ *    
+ * @ingroup tripal_project_api
+ */     
+function chado_autocomplete_project($string = '') {
+  $items = array();
+  $sql = "
+    SELECT
+      B.project_id as id, B.name
+    FROM {project} B
+    WHERE lower(B.name) like lower(:str)
+    ORDER by B.name
+    LIMIT 25 OFFSET 0
+  "; 
+  $records = chado_query($sql, array(':str' => $string . '%'));
+  while ($r = $records->fetchObject()) {
+    $key = "$r->name [id: $r->id]";
+    $items[$key] = "$r->name";
+  }
+  
+  drupal_json_output($items);
+}

+ 34 - 0
tripal_chado/api/modules/tripal_chado.stock.api.inc

@@ -17,6 +17,40 @@
  * @}
  */
 
+/**
+ * Used for autocomplete in forms for identifying stocks
+ *
+ * @param $string
+ *   The string to search for.
+ *
+ * @return
+ *   A json array of terms that begin with the provided string.
+ *
+ * @ingroup tripal_stock_api
+ */
+function chado_autocomplete_stock($string = '') {
+  $items = array();
+  $sql = "
+    SELECT
+      B.stock_id as id, B.uniquename, B.name,
+      O.genus, O,species,
+      CVT.name as type
+    FROM {stock} B
+      INNER JOIN {organism} O ON O.organism_id = B.organism_id
+      INNER JOIN {cvterm} CVT ON CVT.cvterm_id = B.type_id
+    WHERE lower(B.uniquename) like lower(:str) OR lower(B.name) like lower(:str)
+    ORDER by B.name
+    LIMIT 25 OFFSET 0
+  ";
+  $records = chado_query($sql, array(':str' => $string . '%'));
+  while ($r = $records->fetchObject()) {
+    $key = "$r->name [id: $r->id]";
+    $items[$key] = "$r->name ($r->uniquename, $r->type, $r->genus $r->species)";
+  }
+
+  drupal_json_output($items);
+}
+
 /**
  * Retrieves a chado stock variable
  *

+ 457 - 276
tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship.inc

@@ -65,11 +65,33 @@ class sbo__relationship extends ChadoField {
   // An array containing details about the field. The format of this array
   // is the same as that returned by field_info_fields()
   protected $field;
+
   // An array containing details about an instance of the field. A field does
   // not have to have an instance.  But if dealing with an instance (such as
   // when using the widgetForm, formatterSettingsForm, etc.) it should be set.
   protected $instance;
 
+  // An array of columns to use as the "name" of the subject and object.
+  // For example, for the feature table, this will be the name,
+  // 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;
+
+  // This field depends heavily on the schema of the relationship and base
+  // table. The following variables cache the schema to greatly speed up
+  // this field.
+  // Note: both are ChadoSchema objects.
+  protected $schema;
+  protected $base_schema;
+
+  // The column which indicated the subject/object_id in the current
+  // relationship table. This allows us to support exceptions in the common
+  // chado naming conventions.
+  protected $subject_id_column;
+  protected $object_id_column;
 
   /**
    * @see TripalField::elements()
@@ -183,155 +205,238 @@ class sbo__relationship extends ChadoField {
     );
   }
 
-  private function loadRelationship($relationship, &$entity, $delta) {
-
-    $field_name = $this->field['field_name'];
-    $field_table = $this->instance['settings']['chado_table'];
-    $base_table = $this->instance['settings']['base_table'];
-
-    $rel_acc = $relationship->type_id->dbxref_id->db_id->name . ':' . $relationship->type_id->dbxref_id->accession;
-    $rel_type = $relationship->type_id->name;
-    $verb = $this->get_rel_verb($rel_type);
+  /**
+   * Extends TripalField::__construct().
+   */
+  public function __construct($field, $instance) {
+    parent::__construct($field, $instance);
 
-    // Get the foreign keys for the subject and object tables
-    $subject_fkey_table = '';
-    $object_fkey_table = '';
+    $reltable = $instance['settings']['chado_table'];
+    $base_table = $instance['settings']['base_table'];
 
-    $schema = chado_get_schema($field_table);
-    $pkey = $schema['primary key'][0];
-    $fkey_lcolumn = key($schema['foreign keys'][$base_table]['columns']);
-    $fkey_rcolumn = $schema['foreign keys'][$base_table]['columns'][$fkey_lcolumn];
+    // First, initialize the schema's.
+    $this->schema = new ChadoSchema();
+    $this->schema = $this->schema->getTableSchema($reltable);
+    $this->base_schema = new ChadoSchema();
+    $this->base_schema = $this->base_schema->getTableSchema($base_table);
 
-    // Not all tables have the columns named 'subject_id' and 'object_id'.
-    // some have variations on that name and we need to determine what they are.
-    $fkeys = $schema['foreign keys'];
-    $subject_id_key = 'subject_id';
-    $object_id_key = 'object_id';
-    foreach ($fkeys as $fktable => $details) {
-      foreach ($details['columns'] as $fkey_lcolumn => $fkey_rcolumn) {
-        if (preg_match('/^subject_.*id/', $fkey_lcolumn)) {
-          $subject_fkey_table = $fktable;
-          $subject_id_key = $fkey_lcolumn;
-        }
-        if (preg_match('/^object_.*id/', $fkey_lcolumn)) {
-          $object_fkey_table = $fktable;
-          $object_id_key = $fkey_lcolumn;
-        }
+    // Determine the subject_id/object_id column names.
+    foreach ($this->schema['foreign keys'][$base_table]['columns'] AS $lcolum => $rcolum) {
+      if (preg_match('/^subject_.*id/', $lcolum)) {
+        $this->subject_id_column = $lcolum;
+      }
+      else if (preg_match('/^object_.*id/', $lcolum)) {
+        $this->object_id_column = $lcolum;
       }
     }
 
-    // Get the schemas for the subject and object table.  These should
-    // be the same as the base table but just to be safe we'll get them
-    // separately.
-    $subject_schema = chado_get_schema($subject_fkey_table);
-    $subject_pkey = $subject_schema['primary key'][0];
-    $object_schema = chado_get_schema($object_fkey_table);
-    $object_pkey = $object_schema['primary key'][0];
-
-    // Not all realtionshp tables have a name field (e.g. organism_relationship)
-    // threfore in some cases we need to dig a bit deeper to get the entity
-    // name and the entity type name.
-    $subject_name = '';
-    $subject_type = '';
-    $object_name = '';
-    $object_type = '';
-
-    // The linked to table of a relationship linker table may not always
-    // have a type_id or name field.  So we have to be a bit more
-    // specific about how we set some variables.
-    switch ($relationship->tablename) {
+    // Determine the name and type columns.
+    $this->base_name_columns = [];
+    $this->base_type_column = 'table_name';
+    switch ($instance['settings']['chado_table']) {
+
       case 'acquisition_relationship':
-        $subject_type = 'acquisition';
-        $object_type = 'acquisition';
-        break;
       case 'analysis_relationship':
-        $subject_type = 'analysis';
-        $object_type = 'analysis';
-        break;
       case 'biomaterial_relationship':
-        $subject_type = 'biomaterial';
-        $object_type = 'biomaterial';
-        break;
       case 'cell_line_relationship':
-        $subject_type = 'cell_line';
-        $object_type = 'cell_line';
+      case 'quantification_relationship':
+        $this->base_type_column = 'table_name';
         break;
       case 'element_relationship':
-        $subject_name = $relationship->$subject_id_key->feature_id->name;
-        $object_name = $relationship->$object_id_key->feature_id->name;
+        // RELATIONSHIP->subject_id_key->feature_id->name;
+        $this->base_name_columns = ['name'];
+        $this->base_type_column = 'table_name';
         break;
       case 'organism_relationship':
-        $subject_name = $relationship->$subject_id_key->genus . ' ' . $relationship->$subject_id_key->species;
-        $object_name = $relationship->$object_id_key->genus . ' ' . $relationship->$object_id_key->species;
-        $subject_type = 'organism';
-        $object_type = 'organism';
+        $this->base_name_columns = ['genus','species'];
+        $this->base_type_column = 'table_name';
         break;
       case 'project_relationship':
-        $subject_type = 'project';
-        $object_type = 'project';
+        $this->base_name_columns = ['name'];
+        $this->base_type_column = 'table_name';
         break;
       case 'phylonode_relationship':
-        $subject_name = $relationship->$subject_id_key->label;
-        $object_name = $relationship->$object_id_key->label;
+        $this->base_name_columns = ['label'];
+        $this->base_type_column = 'table_name';
         break;
       case 'pub_relationship':
-        $subject_name = $relationship->$subject_id_key->uniquename;
-        $object_name = $relationship->$object_id_key->uniquename;
+        $this->base_name_columns = ['name'];
+        $this->base_type_column = 'table_name';
         break;
-      case 'quantification_relationship':
-        $subject_type = 'quantification';
-        $object_type = 'quantification';
+      case 'contact':
+        $this->base_name_columns = ['name'];
+        $this->base_type_column = 'type_id';
         break;
       default:
-        $subject_name = isset($relationship->$subject_id_key->name) ? $relationship->$subject_id_key->name : '';
-        $subject_type = isset($relationship->$subject_id_key->type_id) ? $relationship->$subject_id_key->type_id->name : '';
-        $object_name = isset($relationship->$object_id_key->name) ? $relationship->$object_id_key->name : '';
-        $object_type = isset($relationship->$object_id_key->type_id) ? $relationship->$object_id_key->type_id->name : '';
+        // @todo update this to use the schema.
+        $this->base_name_columns = ['name'];
+        $this->base_type_column = 'type_id';
     }
+  }
 
-    $entity->{$field_name}['und'][$delta]['value'] = array(
-      'local:relationship_subject' => array(
-        'rdfs:type' => $subject_type,
-        'schema:name' => $subject_name,
-      ),
-      'local:relationship_type' => $relationship->type_id->name,
-      'local:relationship_object' => array(
-        'rdfs:type' => $object_type,
-        'schema:name' => $object_name,
-        'entity' => 'TripalEntity:' . $entity->id,
-      )
-    );
+  /**
+   * Retrive the subject from the current relationship.
+   *
+   * @param $relationship
+   *   A single expanded relationship from a variable generated by chado_generate_var().
+   *   At a minimum, if will have a subject, object and type which should be expanded to
+   *   the appropriate type of record depending on the content type this widget
+   *   is attached to.
+   * @return
+   *   An array of information for the subject of the $relationship.
+   */
+  private function getRelationshipSubject($relationship) {
+    $name = [];
 
-    // If the subject or object have a unqiuename then add that in for refernce.
-    if (property_exists($relationship->$subject_id_key, 'uniquename')) {
-      $entity->{$field_name}['und'][$delta]['value']['local:relationship_subject']['data:0842'] = $relationship->$subject_id_key->uniquename;
+    foreach ($this->base_name_columns as $column) {
+      $name[] = $relationship->{$this->subject_id_column}->{$column};
     }
-    if (property_exists($relationship->$object_id_key, 'uniquename')) {
-      $entity->{$field_name}['und'][$delta]['value']['local:relationship_object']['data:0842'] = $relationship->$object_id_key->uniquename;
+
+    // Retrieve the type.
+    $type = $this->instance['settings']['base_table'];
+    if ((!$this->base_type_column == 'table_name') AND isset($relationship->{$this->subject_id_column}->{$this->base_type_column})) {
+      $type_object = $relationship->{$this->subject_id_column}->{$this->base_type_column};
+      if (isset($type_object->name)) {
+        $type = $type_object->name;
+      }
+      elseif (isset($type_object->uniquename)) {
+        $type = $type_object->uniquename;
+      }
     }
 
-    // If the subject or object have an organism then add that in for reference.
-    if (property_exists($relationship->$subject_id_key, 'organism_id')) {
-      $entity->{$field_name}['und'][$delta]['value']['local:relationship_subject']['OBI:0100026'] = $relationship->$subject_id_key->organism_id->genus . ' ' . $relationship->$subject_id_key->organism_id->species;
+    $record = [
+      'rdfs:type' => $type,
+      'schema:name' => implode(' ', $name),
+      // @todo support the entity and determine whether this is current one or not.
+      //'entity' => 'TripalEntity:' . $entity->id,
+    ];
+
+    // If the object has a uniquename then add that in for refernce.
+    if (property_exists($relationship->{$this->subject_id_column}, 'uniquename')) {
+      $record['data:0842'] = $relationship->{$this->subject_id_column}->uniquename;
     }
-    if (property_exists($relationship->$object_id_key, 'organism_id')) {
-      $entity->{$field_name}['und'][$delta]['value']['local:relationship_object']['OBI:0100026'] = $relationship->$object_id_key->organism_id->genus . ' ' . $relationship->$object_id_key->organism_id->species;
+
+    // If the object has an organism then add that in for reference.
+    if (property_exists($relationship->{$this->subject_id_column}, 'organism_id')
+      AND is_object($relationship->{$this->subject_id_column}->organism_id)) {
+        $record['OBI:0100026'] = $relationship->{$this->subject_id_column}->organism_id->genus . ' ' . $relationship->{$this->subject_id_column}->organism_id->species;
     }
 
-    // Add in the TripalEntity ids if these base records in the relationship
-    // are published.
-    if (property_exists($relationship->$subject_id_key, 'entity_id')) {
-      $entity_id = $relationship->$subject_id_key->entity_id;
-      $entity->{$field_name}['und'][$delta]['value']['local:relationship_subject']['entity'] = 'TripalEntity:' . $entity_id;
+    // Add in the TripalEntity ids if the object is published.
+    if (property_exists($relationship->{$this->subject_id_column}, 'entity_id')) {
+      $entity_id = $relationship->{$this->subject_id_column}->entity_id;
+      $record['entity'] = 'TripalEntity:' . $entity_id;
     }
-    if (property_exists($relationship->$object_id_key, 'entity_id')) {
-      $entity_id = $relationship->$object_id_key->entity_id;
-      $entity->{$field_name}['und'][$delta]['value']['local:relationship_object']['entity'] = 'TripalEntity:' . $entity_id;
+
+    return $record;
+  }
+
+  /**
+   * Retrieve the object from the current relationship.
+   *
+   * @param $relationship
+   *   A single expanded relationship from a variable generated by chado_generate_var().
+   *   At a minimum, if will have a subject, object and type which should be expanded to
+   *   the appropriate type of record depending on the content type this widget
+   *   is attached to.
+   * @return
+   *   An array of information for the object of the $relationship.
+   */
+  private function getRelationshipObject($relationship) {
+    $name = [];
+
+    // Retrieve the name (may be multiple parts).
+    foreach ($this->base_name_columns as $column) {
+      $name[] = $relationship->{$this->object_id_column}->{$column};
     }
 
+    // Retrieve the Type.
+    $type = $this->instance['settings']['base_table'];
+    if ((!$this->base_type_column == 'table_name') AND isset($relationship->{$this->object_id_column}->{$this->base_type_column})) {
+      $type_object = $relationship->{$this->object_id_column}->{$this->base_type_column};
+      if (isset($type_object->name)) {
+        $type = $type_object->name;
+      }
+      elseif (isset($type_object->uniquename)) {
+        $type = $type_object->uniquename;
+      }
+    }
+
+    $record = [
+      'rdfs:type' => $type,
+      'schema:name' => implode(' ', $name),
+      // @todo support the entity and determine whether this is current one or not.
+      //'entity' => 'TripalEntity:' . $entity->id,
+    ];
+
+    // If the object has a unqiuename then add that in for reference.
+    if (property_exists($relationship->{$this->object_id_column}, 'uniquename')) {
+      $record['data:0842'] = $relationship->{$this->object_id_column}->uniquename;
+    }
+
+    // If the object has an organism then add that in for reference.
+    if (property_exists($relationship->{$this->object_id_column}, 'organism_id')
+      AND is_object($relationship->{$this->object_id_column}->organism_id)) {
+        $record['OBI:0100026'] = $relationship->{$this->object_id_column}->organism_id->genus . ' ' . $relationship->{$this->object_id_column}->organism_id->species;
+    }
+
+    // Add in the TripalEntity ids if the object is published.
+    if (property_exists($relationship->{$this->object_id_column}, 'entity_id')) {
+      $entity_id = $relationship->{$this->object_id_column}->entity_id;
+      $record['entity'] = 'TripalEntity:' . $entity_id;
+    }
+
+    return $record;
+  }
+
+  /**
+   * Load a specific relationship as indicated by $delta.
+   * This function is called by the load method below.
+   *
+   * Note: The relationship is loaded by adding it the the entitiy values.
+   *
+   * @param $relationship
+   *   A single expanded relationship from a variable generated by chado_generate_var().
+   *   At a minimum, if will have a subject, object and type which should be expanded to
+   *   the appropriate type of record depending on the content type this widget
+   *   is attached to.
+   * @param $entity
+   *   The entity the widget is attached to.
+   * @param $delta
+   *   An integer indicating the specific relationship to load. This is usually the rank
+   *   from the relationship table (if there is one).
+   */
+  private function loadRelationship($relationship, &$entity, $delta) {
+
+    $field_name = $this->field['field_name'];
+    $field_table = $this->instance['settings']['chado_table'];
+    $base_table = $this->instance['settings']['base_table'];
+
+    $rel_acc = $relationship->type_id->dbxref_id->db_id->name . ':' . $relationship->type_id->dbxref_id->accession;
+    $rel_type = $relationship->type_id->name;
+    $verb = $this->get_rel_verb($rel_type);
+
+    $pkey = $this->schema['primary key'][0];
+    $subject_id_key = $this->subject_id_column;
+    $object_id_key = $this->object_id_column;
+    // @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(
+      'local:relationship_subject' => $this->getRelationshipSubject($relationship),
+      'local:relationship_type' => $relationship->type_id->name,
+      'local:relationship_object' => $this->getRelationshipObject($relationship),
+    );
+
     // Add the clause to the values array.  The clause is a written version
     // of the relationships.
     $rel_type_clean = lcfirst(preg_replace('/_/', ' ', $rel_type));
+    $subject_type = $entity->{$field_name}['und'][$delta]['value']['local:relationship_subject']['rdfs:type'];
+    $subject_name = $entity->{$field_name}['und'][$delta]['value']['local:relationship_subject']['schema:name'];
+    $object_type = $entity->{$field_name}['und'][$delta]['value']['local:relationship_object']['rdfs:type'];
+    $object_name = $entity->{$field_name}['und'][$delta]['value']['local:relationship_object']['schema:name'];
+
+
     // Remember the current entity could be either the subject or object!
     // Example: The genetic_marker, MARKER1 , derives from the sequence_variant, VARIANT1.
     // The above relationship will be shown both on marker and variant pages
@@ -355,14 +460,15 @@ class sbo__relationship extends ChadoField {
     $entity->{$field_name}['und'][$delta]['type_name'] = $relationship->type_id->name;
     $entity->{$field_name}['und'][$delta]['subject_name'] = $subject_name . ' [id: ' . $relationship->$subject_id_key->$subject_pkey . ']';
     $entity->{$field_name}['und'][$delta]['object_name'] = $object_name  . ' [id: ' . $relationship->$object_id_key->$object_pkey . ']';
-    if (array_key_exists('value', $schema['fields'])) {
+    if (array_key_exists('value', $this->schema['fields'])) {
       $entity->{$field_name}['und'][$delta]['chado-' . $field_table . '__value'] = $relationship->value;
     }
-    if (array_key_exists('rank', $schema['fields'])) {
+    if (array_key_exists('rank', $this->schema['fields'])) {
       $entity->{$field_name}['und'][$delta]['chado-' . $field_table . '__rank'] = $relationship->rank;
     }
   }
- /**
+
+  /**
    *
    * @see TripalField::load()
    */
@@ -370,41 +476,26 @@ class sbo__relationship extends ChadoField {
     $settings = $this->field['settings'];
 
     $record = $entity->chado_record;
-    $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
 
     $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'];
+    $rel_table = $field_table;
 
     // Get the PKey for this table
-    $schema = chado_get_schema($field_table);
-    $pkey = $schema['primary key'][0];
-    $fkey_lcolumn = key($schema['foreign keys'][$base_table]['columns']);
-    $fkey_rcolumn = $schema['foreign keys'][$base_table]['columns'][$fkey_lcolumn];
+    $pkey = $this->schema['primary key'][0];
+    // Not all tables have the columns named 'subject_id' and 'object_id'.
+    // some have variations on that name and we need to determine what they are.
+    $subject_id_key = $this->subject_id_column;
+    $object_id_key = $this->object_id_column;
 
     // If we don't have a chado record return before creating a stub for this field!
     if (!$record) {
       return;
     }
 
-    // Not all tables have the columns named 'subject_id' and 'object_id'.
-    // some have variations on that name and we need to determine what they are.
-    $fkeys = $schema['foreign keys'];
-    $subject_id_key = 'subject_id';
-    $object_id_key = 'object_id';
-    foreach ($fkeys as $fktable => $details) {
-      foreach ($details['columns'] as $fkey_lcolumn => $fkey_rcolumn) {
-        if (preg_match('/^subject_.*id/', $fkey_lcolumn)) {
-          $subject_id_key = $fkey_lcolumn;
-        }
-        if (preg_match('/^object_.*id/', $fkey_lcolumn)) {
-          $object_id_key = $fkey_lcolumn;
-        }
-      }
-    }
-
     // Set some defaults for the empty record.
     $entity->{$field_name}['und'][0] = array(
       'value' => '',
@@ -422,18 +513,13 @@ class sbo__relationship extends ChadoField {
 
     // If the table has rank and value fields then add those to the default
     // value array.
-    if (array_key_exists('value', $schema['fields'])) {
+    if (array_key_exists('value', $this->schema['fields'])) {
       $entity->{$field_name}['und'][0]['chado-' . $field_table . '__value'] = '';
     }
-    if (array_key_exists('rank', $schema['fields'])) {
+    if (array_key_exists('rank', $this->schema['fields'])) {
       $entity->{$field_name}['und'][0]['chado-' . $field_table . '__rank'] = '';
     }
 
-    // If we have no record then just return.
-    if (!$record) {
-      return;
-    }
-
     // Expand the object to include the relationships.
     $options = array(
       'return_array' => 1,
@@ -451,9 +537,7 @@ class sbo__relationship extends ChadoField {
         ),
       ),
     );
-    $rel_table = $base_table . '_relationship';
-    $schema = chado_get_schema($rel_table);
-    if (array_key_exists('rank', $schema['fields'])) {
+    if (array_key_exists('rank', $this->schema['fields'])) {
       $options['order_by'] = array('rank' => 'ASC');
     }
     $record = chado_expand_var($record, 'table', $rel_table, $options);
@@ -518,15 +602,18 @@ class sbo__relationship extends ChadoField {
     }
 
     // Filter by unique name of the subject or object.
-    if ($condition['column'] == $rel_subject_identifier) {
-      $this->queryJoinOnce($query, $chado_table, $alias, "base.$bpkey = $alias.object_id");
-      $this->queryJoinOnce($query, $base_table, 'base2', "base2.$bpkey = $alias.subject_id");
-      $query->condition("base2.uniquename", $condition['value'], $operator);
-    }
-    if ($condition['column'] == $rel_object_identifier) {
-      $this->queryJoinOnce($query, $chado_table, $alias, "base.$bpkey = $alias.subject_id");
-      $this->queryJoinOnce($query, $base_table, 'base2', "base2.$bpkey = $alias.object_id");
-      $query->condition("base2.uniquename", $condition['value'], $operator);
+    // If this table has a uniquename!
+    if (isset($this->schema['fields']['uniquename'])) {
+      if ($condition['column'] == $rel_subject_identifier) {
+        $this->queryJoinOnce($query, $chado_table, $alias, "base.$bpkey = $alias.object_id");
+        $this->queryJoinOnce($query, $base_table, 'base2', "base2.$bpkey = $alias.subject_id");
+        $query->condition("base2.uniquename", $condition['value'], $operator);
+      }
+      if ($condition['column'] == $rel_object_identifier) {
+        $this->queryJoinOnce($query, $chado_table, $alias, "base.$bpkey = $alias.subject_id");
+        $this->queryJoinOnce($query, $base_table, 'base2', "base2.$bpkey = $alias.object_id");
+        $query->condition("base2.uniquename", $condition['value'], $operator);
+      }
     }
 
     // Filter by the type of the subject or object
@@ -834,151 +921,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 = isset($item['subject_name']) ? $item['subject_name'] : '';
+    $object_name = isset($item['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 = isset($item['subject_name']) ? $items['subject_name'] : '';
-      $object_name = isset($item['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 {
-          $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 {
-          $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;
   }
 
 
@@ -989,4 +1136,38 @@ class sbo__relationship extends ChadoField {
   public function queryOrder($query, $order) {
 
   }
+
+  /**
+   * Retrieve the schema's
+   */
+  public function getRelTableSchema() {
+    return $this->schema;
+  }
+  public function getBaseTableSchema() {
+    return $this->base_schema;
+  }
+
+  /**
+   * Retrieve the subject/object key columns.
+   */
+  public function getSubjectIdColumn() {
+    return $this->subject_id_column;
+  }
+  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;
+  }
 }

+ 404 - 253
tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship_widget.inc

@@ -7,6 +7,59 @@ class sbo__relationship_widget extends ChadoFieldWidget {
   // The list of field types for which this formatter is appropriate.
   public static $field_types = array('sbo__relationship');
 
+  // --------------------------------------------------------------------------
+  //              PROTECTED CLASS MEMBERS -- DO NOT OVERRIDE
+  // --------------------------------------------------------------------------
+
+  // This field depends heavily on the schema of the relationship and base
+  // table. The following variables cache the schema to greatly speed up
+  // this field.
+  // Note: both are ChadoSchema objects.
+  protected $schema;
+  protected $base_schema;
+
+  // The column which indicated the subject/object_id in the current
+  // relationship table. This allows us to support exceptions in the common
+  // chado naming conventions.
+  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 name,
+  // 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;
+
+  /**
+   * Extends TripalField::__construct().
+   */
+  public function __construct($field, $instance) {
+    parent::__construct($field, $instance);
+
+    module_load_include('inc', 'tripal_chado', 'includes/TripalFields/sbo__relationship/sbo__relationship');
+    $this->field_instance = new sbo__relationship($field, $instance);
+
+    // Retrieve the schema's.
+    $this->schema = $this->field_instance->getRelTableSchema();
+    $this->base_schema = $this->field_instance->getBaseTableSchema();
+
+    // Retrieve the subject/object column names.
+    $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();
+
+  }
+
   /**
    *
    * @see TripalFieldWidget::form()
@@ -14,120 +67,121 @@ class sbo__relationship_widget extends ChadoFieldWidget {
   public function form(&$widget, &$form, &$form_state, $langcode, $items, $delta, $element) {
     parent::form($widget, $form, $form_state, $langcode, $items, $delta, $element);
 
-    // TODO: make this widget deal better with the various relationship
-    // tables. See the load function as it does a better job of this.
-    return;
-
     // Get the field settings.
     $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'];
+    $widget['#table_name'] = $field_table;
 
-    // Get the FK column that links to the base table.
-    $base_table = $this->instance['settings']['base_table'];
-    $schema = chado_get_schema($field_table);
-    $pkey = $schema['primary key'][0];
-    $fkeys = array_values($schema['foreign keys'][$base_table]['columns']);
-    $fkey = $fkeys[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'];
-    }
 
-    // For testing if there are selected vocabs for option1 we'll copy the
-    // contents in a special variable for later.
-    $option1_test = $option1_vocabs;
+    // Get the primary key of the relationship table
+    $pkey = $this->schema['primary key'][0];
 
-    // Get the field defaults.
+    // '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;
+
+    // Default Values:
+    //----------------
     $record_id = '';
     $subject_id = '';
     $object_id = '';
     $type_id = '';
     $value = '';
     $rank = '';
-    $subject_uniquename = '';
-    $object_uniquename = '';
+    $subject_label = '';
+    $object_label = '';
     $type = '';
 
-    // '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;
-      }
-    }
-
     // 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)) {
-      // Check for element values that correspond to fields in the Chado table.
-      $record_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__' . $pkey, $record_id);
-      $subject_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__' . $subject_id_key, $subject_id);
-      $object_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__' . $object_id_key, $object_id);
+
+      // Sometimes empty/initialized items are getting through.
+      // To determine if it this one of them, the type_id must always be there.
       $type_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__type_id', $type_id);
-      // Not all Chado tables have a value and rank.  So we'll only get
-      // those if applicable.
-      if (array_key_exists('value', $schema['fields'])) {
-        $value = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__value', $value);
-      }
-      if (array_key_exists('rank', $schema['fields'])) {
-        $rank = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__rank', $rank);
+      if (!empty($type_id)) {
+
+        // Check for element values that correspond to fields in the Chado table.
+        $record_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__' . $pkey, $record_id);
+        $subject_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__' . $subject_id_key, $subject_id);
+        $object_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__' . $object_id_key, $object_id);
+        $type_id = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__type_id', $type_id);
+
+        // Not all Chado tables have a value and rank.  So we'll only get
+        // those if applicable.
+        if (array_key_exists('value', $this->schema['fields'])) {
+          $value = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__value', $value);
+        }
+        if (array_key_exists('rank', $this->schema['fields'])) {
+          $rank = tripal_get_field_item_keyval($items, $delta, 'chado-' . $field_table . '__rank', $rank);
+        }
+
+        // Get element values added to help support insert/updates.
+        $object_label = tripal_get_field_item_keyval($items, $delta, 'object_name', $object_label);
+        $subject_label = tripal_get_field_item_keyval($items, $delta, 'subject_name', $subject_label);
+        $type = tripal_get_field_item_keyval($items, $delta, 'type_name', $type);
+
       }
-      // Get element values added to help support insert/updates.
-      $object_uniquename = tripal_get_field_item_keyval($items, $delta, 'object_name', $object_uniquename);
-      $subject_uniquename = tripal_get_field_item_keyval($items, $delta, 'subject_name', $subject_uniquename);
-      $type = tripal_get_field_item_keyval($items, $delta, 'type_name', $type);
     }
 
     // Check $form_state['values'] to see if an AJAX call set the values.
     if (array_key_exists('values', $form_state) and
-      array_key_exists($field_name, $form_state['values'])) {
+        array_key_exists($field_name, $form_state['values'])) {
       $record_id = $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__' . $pkey];
       $subject_id = $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__' . $subject_id_key];
       $object_id = $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__' . $object_id_key];
       $type_id = $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__type_id'];
-      if (array_key_exists('value', $schema['fields'])) {
+      if (array_key_exists('value', $this->schema['fields'])) {
         $value = $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__value'];
       }
-      if (array_key_exists('rank', $schema['fields'])) {
+      if (array_key_exists('rank', $this->schema['fields'])) {
         $rank = $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__rank'];
       }
-      $object_uniquename = $form_state['values'][$field_name]['und'][$delta]['object_name'];
-      $subject_uniquename = $form_state['values'][$field_name]['und'][$delta]['subject_name'];
+      $object_label = $form_state['values'][$field_name]['und'][$delta]['object_name'];
+      $subject_label = $form_state['values'][$field_name]['und'][$delta]['subject_name'];
       $type = $form_state['values'][$field_name]['und'][$delta]['type_name'];
+
+    }
+
+    // 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'] = $schema['foreign keys'];
+    $widget['#fkeys'] = $this->schema['foreign keys'];
     $widget['#base_table'] = $base_table;
     $widget['#chado_record_id'] = isset($form['#entity']) ? $form['#entity']->chado_record_id : '';
     //$widget['#element_validate'] = array('sbo__relationship_validate');
     $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'] : '',
@@ -148,131 +202,44 @@ class sbo__relationship_widget extends ChadoFieldWidget {
       '#type' => 'value',
       '#default_value' => $object_id,
     );
-    if (array_key_exists('value', $schema['fields'])) {
+    if (array_key_exists('value', $this->schema['fields'])) {
       $widget['chado-' . $field_table . '__value'] = array(
         '#type' => 'value',
         '#default_value' => $value,
       );
     }
-    if (array_key_exists('rank', $schema['fields'])) {
+    if (array_key_exists('rank', $this->schema['fields'])) {
       $widget['chado-' . $field_table . '__rank'] = array(
         '#type' => 'value',
         '#default_value' => $rank,
       );
     }
+
+    // Subject:
+    //----------
     $widget['subject_name'] = array(
       '#type' => 'textfield',
       '#title' => t('Subject'),
-      '#default_value' => $subject_uniquename,
+      '#default_value' => $subject_label,
       '#required' => $element['#required'],
-      '#maxlength' => array_key_exists($subject_id_key, $schema['fields']) && array_key_exists('length', $schema['fields'][$subject_id_key]) ? $schema['fields'][$subject_id_key]['length'] : 255,
+      '#maxlength' => array_key_exists($subject_id_key, $this->schema['fields']) && array_key_exists('length', $this->schema['fields'][$subject_id_key]) ? $this->schema['fields'][$subject_id_key]['length'] : 255,
       '#size' => 35,
-      '#autocomplete_path' => "admin/tripal/storage/chado/auto_name/$base_table",
     );
-
-    // 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'];
+    // Add autocomplete if we have one for this base table.
+    if ($has_autocomplete) {
+      $widget['subject_name']['#autocomplete_path'] = $autocomplete_path;
     }
 
-    $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);
+    // Type:
+    //-------
+    $rtype_options = $this->get_rtype_select_options();
+    if ($rtype_options) {
 
-      // 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)) {
@@ -280,11 +247,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));
@@ -302,6 +272,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",
@@ -309,27 +280,33 @@ class sbo__relationship_widget extends ChadoFieldWidget {
           'method' => 'replace'
         ),
       );
+      $widget['type_name'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Relationship Type'),
+        '#size' => 15,
+        '#default_value' => $default_term,
+        '#disabled' => TRUE,
+        '#autocomplete_path' => "admin/tripal/storage/chado/auto_name/cvterm/$cv_id"
+      );
       if ($cv_id) {
-        $options = array();
-        $widget['type_name'] = array(
-          '#type' => 'textfield',
-          '#title' => t('Relationship Type'),
-          '#size' => 15,
-          '#default_value' => $default_term,
-          '#autocomplete_path' => "admin/tripal/storage/chado/auto_name/cvterm/$cv_id"
-        );
+        $widget['type_name']['#disabled'] = FALSE;
       }
     }
 
+    // Object:
+    //--------
     $widget['object_name'] = array(
       '#type' => 'textfield',
       '#title' => t('Object'),
-      '#default_value' => $object_uniquename,
+      '#default_value' => $object_label,
       '#required' => $element['#required'],
-      '#maxlength' => array_key_exists($object_id_key, $schema['fields']) && array_key_exists('length', $schema['fields'][$object_id_key]) ? $schema['fields'][$object_id_key]['length'] : 255,
+      '#maxlength' => array_key_exists($object_id_key, $this->schema['fields']) && array_key_exists('length', $this->schema['fields'][$object_id_key]) ? $this->schema['fields'][$object_id_key]['length'] : 255,
       '#size' => 35,
-      '#autocomplete_path' => "admin/tripal/storage/chado/auto_name/$base_table",
     );
+    // Add autocomplete if we have one for this base table.
+    if ($has_autocomplete) {
+      $widget['object_name']['#autocomplete_path'] = $autocomplete_path;
+    }
   }
 
   /**
@@ -345,97 +322,158 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     $base_table = $this->instance['settings']['base_table'];
     $chado_record_id = array_key_exists('#entity', $element)? $element['#entity']->chado_record_id : NULL;
 
-    $schema = chado_get_schema($field_table);
-    $fkeys = $schema['foreign keys'];
+    $fkeys = $this->schema['foreign keys'];
 
     // '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/object_id. Retrieve the column names determined in the form.
+    $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'] : '';
     $type_name = array_key_exists('type_name', $form_state['values'][$field_name][$langcode][$delta]) ? $form_state['values'][$field_name][$langcode][$delta]['type_name'] : '';
     $subject_id = isset($form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $subject_id_key]) ? $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $subject_id_key] : '';
     $object_id = isset($form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $object_id_key]) ? $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__' . $object_id_key]: '';
     $type_id = isset($form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__type_id']) ? $form_state['values'][$field_name][$langcode][$delta]['chado-' . $field_table . '__type_id'] : '';
 
-    $subject_name = isset($form_state['values'][$field_name][$langcode][$delta]['subject_name']) ? $form_state['values'][$field_name][$langcode][$delta]['subject_name']: '';
+    $subject_name = isset($form_state['values'][$field_name][$langcode][$delta]['subject_name']) ? $form_state['values'][$field_name][$langcode][$delta]['subject_name'] : '';
     $object_name = isset($form_state['values'][$field_name][$langcode][$delta]['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 || $type_name) && !$subject_name && !$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'])) {
+        !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();
-    if (preg_match('/\[id: (\d+)\]/', $subject_name, $matches)) {
-      $subject_id =  $matches[1];
-    }
-    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':
+            form_set_error('sbo__relationship]['.$langcode.']['.$delta, $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();
-    if (preg_match('/\[id: (\d+)\]/', $object_name, $matches)) {
-      $object_id = $matches[1];
+    // Ensure data is prepared for the storage backend:
+    //-------------------------------------------------
     }
     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 ($subject_id && $object_id && $type_id) {
-      // Set the IDs according to the values that were determined above.
-      $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', $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];
       }
-    }
-    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', $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;
+        }
+      }
+
+      // 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, maybe we are creating the entity...
+      // The storage API sohuld handle this case and automagically add the key in once
+      // the chado record is created... so all we need to do is set the other columns.
+      elseif ($subject_name && $object_id && $type_id) {
+        $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 . '__' . $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'];
+        }
+      }
+      elseif ($subject_id && $object_name && $type_id) {
+        $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 . '__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, we don't have a vallue to insert so 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'] = '';
+        }
       }
     }
+
+    return $errors;
   }
 
   /**
@@ -445,25 +483,138 @@ class sbo__relationship_widget extends ChadoFieldWidget {
     $layout = "
       <div class=\"chado-linker--relationship-widget\">
         <div class=\"chado-linker--relationship-widget-item\">" .
-      drupal_render($element['subject_name']) . "
+          drupal_render($element['subject_name']) . "
         </div>
         <div class=\"chado-linker--relationship-widget-item\">" .
-      drupal_render($element['vocabulary']) . "
+          drupal_render($element['vocabulary']) . "
         </div>
         <div class=\"chado-linker--relationship-widget-item\">" .
-      drupal_render($element['type_name']) . "
+          drupal_render($element['type_name']) . "
         </div>
         <div class=\"chado-linker--relationship-widget-item\">" .
-      drupal_render($element['type_id']) . "
+          drupal_render($element['type_id']) . "
         </div>
         <div>" .
-      drupal_render($element['object_name']) . "
+          drupal_render($element['object_name']) . "
         </div>
       </div>
     ";
     return $layout;
   }
 
+  /**
+   * Retrieve options for the type drop-down for the relationship widget.
+   */
+  public function get_rtype_select_options() {
+
+    // This is slated for Release 2 of this widget.
+    // It still needs extensive functional and automated testing.
+    // Thus for now we are falling back on the Default option:
+    // Form will provide a type autocomplete + vocab select.
+    // @todo test this.
+    return FALSE;
+
+    // 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) {

+ 17 - 0
tripal_chado/tripal_chado.module

@@ -36,6 +36,7 @@ require_once 'api/tripal_chado.schema_v1.11.api.inc';
 require_once 'api/tripal_chado.semweb.api.inc';
 require_once 'api/tripal_chado.migrate.api.inc';
 require_once 'api/tripal_chado.DEPRECATED.api.inc';
+require_once 'api/ChadoSchema.inc';
 require_once 'api/ChadoRecord.inc';
 
 // Chado module specific API functions
@@ -570,6 +571,22 @@ function tripal_chado_menu() {
     'file path' => drupal_get_path('module', 'tripal_chado'),
     'type' => MENU_CALLBACK,
   );
+  $items['admin/tripal/storage/chado/auto_name/stock/%'] = array(
+    'page callback' => 'chado_autocomplete_stock',
+    'page arguments' => array(6),
+    'access arguments' => array('access content'),
+    'file' => 'api/modules/tripal_chado.stock.api.inc',
+    'file path' => drupal_get_path('module', 'tripal_chado'),
+    'type' => MENU_CALLBACK,
+  );
+  $items['admin/tripal/storage/chado/auto_name/project/%'] = array(
+    'page callback' => 'chado_autocomplete_project',
+    'page arguments' => array(6),
+    'access arguments' => array('access content'),
+    'file' => 'api/modules/tripal_chado.project.api.inc',
+    'file path' => drupal_get_path('module', 'tripal_chado'),
+    'type' => MENU_CALLBACK,
+  );
 
   //////////////////////////////////////////////////////////////////////////////
   //                          Controlled Vocabularies