Selaa lähdekoodia

Merge branch '7.x-3.x' into 77-gff_optional_proteins

Bradford Condon 6 vuotta sitten
vanhempi
commit
0fd603b9d5
27 muutettua tiedostoa jossa 697 lisäystä ja 247 poistoa
  1. 2 0
      README.md
  2. 3 1
      composer.json
  3. 8 7
      composer.lock
  4. 23 0
      tests/DataFactory.php
  5. 1 0
      tests/README.md
  6. 46 3
      tests/tripal/api/TripalJobsApiTest.php
  7. 26 19
      tests/tripal_chado/api/TripalChadoAPITest.php
  8. 7 7
      tripal/api/tripal.jobs.api.inc
  9. 4 2
      tripal/api/tripal.terms.api.inc
  10. 21 1
      tripal/includes/TripalEntity.inc
  11. 93 40
      tripal/includes/TripalEntityController.inc
  12. 16 3
      tripal/includes/TripalEntityUIController.inc
  13. 0 7
      tripal/includes/TripalFieldQuery.inc
  14. 34 6
      tripal/includes/tripal.entity.inc
  15. 116 72
      tripal_chado/api/tripal_chado.api.inc
  16. 1 1
      tripal_chado/api/tripal_chado.query.api.inc
  17. 3 2
      tripal_chado/api/tripal_chado.variables.api.inc
  18. 5 0
      tripal_chado/includes/TripalFields/chado_linker__contact/chado_linker__contact.inc
  19. 5 0
      tripal_chado/includes/TripalFields/sbo__database_cross_reference/sbo__database_cross_reference.inc
  20. 17 2
      tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship.inc
  21. 1 1
      tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship_formatter.inc
  22. 223 0
      tripal_chado/includes/TripalFields/sbo__relationship_table/sbo__relationship_table_formatter.inc
  23. 2 2
      tripal_chado/includes/TripalFields/schema__publication/schema__publication_widget.inc
  24. 10 5
      tripal_chado/includes/TripalFields/sep__protocol/sep__protocol.inc
  25. 11 10
      tripal_chado/includes/TripalFields/sep__protocol/sep__protocol_formatter.inc
  26. 8 54
      tripal_chado/includes/TripalFields/sep__protocol/sep__protocol_widget.inc
  27. 11 2
      tripal_chado/includes/tripal_chado.entity.inc

+ 2 - 0
README.md

@@ -121,3 +121,5 @@ DRUPAL_ROOT=/var/www/html
 Then run PHPUnit from your root Tripal directory.
 
 PHPUnit tests will also be run in the Travis CI build.
+
+Read our [testing guidelines](tests/README.md)

+ 3 - 1
composer.json

@@ -1,7 +1,9 @@
 {
+  "name": "tripal",
+  "description": "Tripal is an toolkit to facilitate construction of online genomic, genetic (and other biological) websites.",
   "require-dev": {
     "doctrine/instantiator": "1.0.*",
-    "statonlab/tripal-test-suite": "^1.3"
+    "statonlab/tripal-test-suite": "1.*"
   },
   "require": {
   }

+ 8 - 7
composer.lock

@@ -1,10 +1,10 @@
 {
     "_readme": [
         "This file locks the dependencies of your project to a known state",
-        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "adeee18e119693b993349d81c922f961",
+    "content-hash": "fd3ab0a8fd06e0532a7764f20a9aa52b",
     "packages": [],
     "packages-dev": [
         {
@@ -1657,16 +1657,17 @@
         },
         {
             "name": "statonlab/tripal-test-suite",
-            "version": "1.3.0",
+
+            "version": "1.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/statonlab/TripalTestSuite.git",
-                "reference": "1e616e426faf46b2c2c7cecc43741ee462387433"
+                "reference": "e469f7fde2cf69303dc22315b142b46b027c9931"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/statonlab/TripalTestSuite/zipball/1e616e426faf46b2c2c7cecc43741ee462387433",
-                "reference": "1e616e426faf46b2c2c7cecc43741ee462387433",
+                "url": "https://api.github.com/repos/statonlab/TripalTestSuite/zipball/e469f7fde2cf69303dc22315b142b46b027c9931",
+                "reference": "e469f7fde2cf69303dc22315b142b46b027c9931",
                 "shasum": ""
             },
             "require": {
@@ -1701,7 +1702,7 @@
                     "email": "bcondon@utk.edu"
                 }
             ],
-            "time": "2018-08-03T14:20:13+00:00"
+            "time": "2018-08-06T19:38:14+00:00"
         },
         {
             "name": "symfony/console",

+ 23 - 0
tests/DataFactory.php

@@ -86,3 +86,26 @@ Factory::define('chado.analysis', function (Faker\Generator $faker) {
     'sourceuri' => $faker->name,
   ];
 });
+/** @see  StatonLab\TripalTestSuite\Database\Factory::define() */
+Factory::define('tripal_jobs', function (Faker\Generator $faker) {
+  return [
+    'uid' => 1,
+    'job_name' => $faker->sentence,
+    'modulename' => $faker->word,
+    'callback' => $faker->word,
+    'arguments' => serialize(['arg' => $faker->word]),
+    'progress' => 0,
+    'status' => $faker->randomElement([
+      'Waiting',
+      'Cancelled',
+      'Error',
+      'Completed',
+    ]),
+    'submit_date' => time(),
+    'start_time' => NULL,
+    'end_time' => NULL,
+    'error_msg' => NULL,
+    'pid' => $faker->numberBetween(1, 10000),
+    'priority' => $faker->numberBetween(1, 10),
+  ];
+}, 'job_id');

+ 1 - 0
tests/README.md

@@ -11,6 +11,7 @@ For a basic introduction of Tripal Testing, please see the [Test Suite repo](htt
 
 After cloning the [Tripal github repo](https://github.com/tripal/tripal), you will need to install the developer dependencies required to run tests locally.  To do this, you'll need to [install Composer](https://getcomposer.org/doc/00-intro.md), and then execute `composer install` in your project root.
 
+Remember to run `composer update` to update TripalTestSuite before writing and running new tests. This is especially important when running pull requests that contribute unit tests. If tests are passing on the Travis environment but not on your machine, running composer update might resolve the problem.
 
 ## Testing criteria
 For facilitate accepting your pull requests, your code should include tests.  The tests should meet the following guidelines:

+ 46 - 3
tests/tripal/api/TripalJobsApiTest.php

@@ -1,10 +1,11 @@
 <?php
+
 namespace Tests\tripal\api;
 
 use StatonLab\TripalTestSuite\DBTransaction;
 use StatonLab\TripalTestSuite\TripalTestCase;
 
-class TripalJobsApiTest extends TripalTestCase {
+class TripalJobsApiTest extends TripalTestCase{
 
   use DBTransaction;
 
@@ -13,8 +14,50 @@ class TripalJobsApiTest extends TripalTestCase {
    *
    * @test
    */
-  public function should_create_a_tripal_job() {
-    $job_id = tripal_add_job('Test adding jobs', 'tripal', 'tripal_tripal_cron_notification', [], 1);
+  public function testCreatingAJobWorks() {
+    $job_id = tripal_add_job('Test adding jobs', 'tripal',
+      'tripal_tripal_cron_notification', [], 1);
     $this->assertTrue(is_numeric($job_id));
   }
+
+  /** @test */
+  public function testRetrievingAJob() {
+    $job = factory('tripal_jobs')->create();
+
+    $job2 = tripal_get_job($job->job_id);
+
+    $this->assertNotEmpty($job2);
+    $this->assertObjectHasAttribute('job_id', $job2);
+    $this->assertEquals($job2->job_id, $job->job_id);
+  }
+
+  /** @test */
+  public function testRetrievingActiveJobs() {
+    factory('tripal_jobs')->create();
+    $jobs = tripal_get_active_jobs();
+
+    $this->assertNotEmpty($jobs);
+  }
+
+  /** @test */
+  public function testRetrievingActiveJobsWithAGivenModule() {
+    factory('tripal_jobs')->create([
+      'modulename' => 'tripal_test_suite',
+      'status' => 'Running',
+    ]);
+    $jobs = tripal_get_active_jobs('tripal_test_suite');
+
+    $this->assertNotEmpty($jobs);
+  }
+
+  /** @test */
+  public function testRetrievingCompletedJobsDoesNotHappen() {
+    factory('tripal_jobs')->create([
+      'modulename' => 'tripal_test_suite',
+      'status' => 'Completed',
+    ]);
+    $jobs = tripal_get_active_jobs('tripal_test_suite');
+
+    $this->assertEmpty($jobs);
+  }
 }

+ 26 - 19
tests/tripal_chado/api/TripalChadoAPITest.php

@@ -1,11 +1,11 @@
 <?php
-namespace Tests\tripal_chado\api;
 
+namespace Tests\tripal_chado\api;
 
 use StatonLab\TripalTestSuite\DBTransaction;
 use StatonLab\TripalTestSuite\TripalTestCase;
 
-class TripalChadoAPITest extends TripalTestCase {
+class TripalChadoAPITest extends TripalTestCase{
 
   use DBTransaction;
 
@@ -17,33 +17,39 @@ class TripalChadoAPITest extends TripalTestCase {
    */
   public function test_tripal_chado_publish_records() {
     $genus_string = 'a_genius_genus';
-    //create an organism, publish it
+
+    // Create an organism, publish it
     $organism = factory('chado.organism')->create([
       'genus' => $genus_string,
       'species' => 'fake_species',
     ]);
-    //get bundle ID for organism
+
+    // Get bundle ID for organism
     $bundle = db_select('public.chado_bundle', 'CB')
       ->fields('CB', ['bundle_id'])
       ->condition('data_table', 'organism')
-      ->execute()->fetchField();
+      ->execute()
+      ->fetchField();
 
-    var_dump($bundle);
     $values = ['bundle_name' => 'bio_data_' . $bundle];
 
- //   ob_start();//dont display the job message
-    $bool = chado_publish_records($values);
-   // ob_end_clean();
+    // Don't display the job message
+    $bool = silent(function () use ($values) {
+      return chado_publish_records($values);
+    });
 
-    $this->assertTrue($bool, 'Publishing a fake organism record failed');
+    $this->assertTrue($bool->getReturnValue(),
+      'Publishing a fake organism record failed');
 
-    //ensure that our entity was created
-    $query = db_select('chado.organism', 'O')
-      ->fields('O', ['organism_id']);
-    $query->join('public.chado_bio_data_' . $bundle, 'CBD', 'O.organism_id = CBD.record_id');
+    // Ensure that our entity was created
+    $query = db_select('chado.organism', 'O')->fields('O', ['organism_id']);
+    $query->join('public.chado_bio_data_' . $bundle, 'CBD',
+      'O.organism_id = CBD.record_id');
     $query->condition('O.genus', $genus_string);
     $organism_id = $query->execute()->fetchField();
-    $this->assertNotNull($organism_id, 'Organism with record ID not found in chado_bio_data table.');
+
+    $this->assertNotNull($organism_id,
+      'Organism with record ID not found in chado_bio_data table.');
   }
 
   /**
@@ -52,10 +58,11 @@ class TripalChadoAPITest extends TripalTestCase {
    * @group api
    */
   public function test_tripal_chado_publish_records_false_with_bad_bundle() {
-    putenv("TRIPAL_SUPPRESS_ERRORS=TRUE");//this will fail, so we suppress the tripal error reporter
-    $bool = chado_publish_records(['bundle_name' => 'never_in_a_million_years']);
-    $this->assertFalse($bool);
-    putenv("TRIPAL_SUPPRESS_ERRORS");//unset
+    $bool = silent(function () {
+      return chado_publish_records(['bundle_name' => 'never_in_a_million_years']);
+    });
+
+    $this->assertFalse($bool->getReturnValue());
   }
 
   /**

+ 7 - 7
tripal/api/tripal.jobs.api.inc

@@ -1,7 +1,7 @@
 <?php
 /**
  * @file
- * Tripal offers a job management subsystem for managing tasks that may require 
+ * Tripal offers a job management subsystem for managing tasks that may require
  * an extended period of time for completion.
  */
 
@@ -39,7 +39,7 @@
  * @param $uid
  *    The uid of the user adding the job
  * @param $priority
- *    The priority at which to run the job where the highest priority is 10 and 
+ *    The priority at which to run the job where the highest priority is 10 and
  *    the lowest priority is 1. The default priority is 10.
  * @param $includes
  *    An array of paths to files that should be included in order to execute
@@ -60,7 +60,7 @@
  *         $user->uid, $analysis_id, $match_type);
  *
  *   $includes = array()
- *   $includes[] = module_load_include('inc', 'tripal_chado', 
+ *   $includes[] = module_load_include('inc', 'tripal_chado',
  *                                'includes/loaders/tripal_chado.fasta_loader');
  *
  *   tripal_add_job("Import FASTA file: $dfile", 'tripal_feature',
@@ -237,7 +237,7 @@ function tripal_max_jobs_exceeded($max_jobs) {
  * @param $job_id
  *   The job_id of the job to be re-ran
  * @param $goto_jobs_page
- *   If set to TRUE then after the re run job is added Drupal will redirect to 
+ *   If set to TRUE then after the re run job is added Drupal will redirect to
  *   the jobs page
  *
  * @ingroup tripal_jobs_api
@@ -337,7 +337,7 @@ function tripal_cancel_job($job_id, $redirect = TRUE) {
  *   job needs to be launched and this argument will allow it.  Only jobs
  *   which have not been run previously will run.
  * @param $max_jobs
- *   The maximum number of jobs that should be run concurrently. If -1 then 
+ *   The maximum number of jobs that should be run concurrently. If -1 then
  *   unlimited.
  * @param $single
  *   Ensures only a single job is run rather then the entire queue.
@@ -533,7 +533,7 @@ function tripal_get_active_jobs($modulename = NULL) {
 
   $jobs = array();
   while($job = $results->fetchobject()) {
-    $jobs->arguments = unserialize($job->arguments);
+    $job->arguments = unserialize($job->arguments);
     $jobs[] = $job;
   }
   return $jobs;
@@ -567,4 +567,4 @@ function tripal_execute_job($job_id, $redirect = TRUE) {
   if ($redirect) {
     drupal_goto("admin/tripal/tripal_jobs/view/$job_id");
   }
-}
+}

+ 4 - 2
tripal/api/tripal.terms.api.inc

@@ -689,7 +689,9 @@ function tripal_get_term_lookup_form(&$form, &$form_state, $default_name = '',
     // TODO: this should not call the chado functions because we're in the
     // tripal module.
     $terms = chado_generate_var('cvterm', $match, array('return_array' => TRUE));
-    $terms = chado_expand_var($terms, 'field', 'cvterm.definition');
+    if ($terms) {
+      $terms = chado_expand_var($terms, 'field', 'cvterm.definition');
+    }
     $num_terms = 0;
     $selected_term = '';  
 
@@ -712,7 +714,7 @@ function tripal_get_term_lookup_form(&$form, &$form_state, $default_name = '',
         '#attributes' => $attrs,
         '#description' => '<b>Vocabulary:</b> ' . $term->cv_id->name . ' (' . $term->dbxref_id->db_id->name . ') ' . $term->cv_id->definition .
         '<br><b>Term: </b> ' . $term->dbxref_id->db_id->name . ':' . $term->dbxref_id->accession . '.  ' .
-        '<br><b>Definition:</b>  ' . $term->definition,
+        '<br><b>Definition:</b>  ' . property_exists($term, 'definition') ? $term->definition : '',
         '#ajax' => array(
           'callback' => "tripal_get_term_lookup_form_ajax_callback",
           'wrapper' => $ajax_wrapper_id,

+ 21 - 1
tripal/includes/TripalEntity.inc

@@ -15,4 +15,24 @@ class TripalEntity extends Entity {
     return array('path' => 'TripalEntity/' . $this->id);
   }
 
-}
+  /**
+   * Permanently saves the entity.
+   *
+   * @param $cache
+   *   This array is used to store objects you want to cache for performance reasons,
+   *   as well as, cache related options. The following are supported:
+   *   - boolean $clear_cached_fields
+   *       Clearing cached fields is NECESSARY. IF you choose to set this to false then YOU
+   *       must clear the cache yourself using cache_clear_all('field:TripalEntity:[entity_id]', 'cache_field', TRUE).
+   *       The only known reason to set this to FALSE is to clear the cache in bulk for perfomance reasons.
+   *   - TripalBundle $bundle
+   *       The bundle for the current entity.
+   *   - TripalTerm $term
+   *       The term for the current entity.
+   * @see entity_save()
+   */
+  public function save($cache = array()) {
+    return entity_get_controller($this->entityType)->save($this, $cache);
+  }
+
+}

+ 93 - 40
tripal/includes/TripalEntityController.inc

@@ -45,7 +45,12 @@ class TripalEntityController extends EntityAPIController {
     $modules = module_implements('entity_create');
     foreach ($modules as $module) {
       $function = $module . '_entity_create';
-      $function($entity, $values['type']);
+      if (isset($values['bundle_object'])) {
+        $function($entity, $values['type'], $values['bundle_object']);
+      }
+      else {
+        $function($entity, $values['type']);
+      }
     }
     return $entity;
 
@@ -102,20 +107,30 @@ class TripalEntityController extends EntityAPIController {
    *   The entity whose title should be changed.
    * @param $title
    *   The title to use. It can contain tokens the correspond to field values.
-   *   Token should be be compatible with those returned by 
+   *   Token should be be compatible with those returned by
    *   tripal_get_entity_tokens().
+   * @param $cache
+   *   This array is used to store objects you want to cache for performance reasons,
+   *   as well as, cache related options. The following are supported:
+   *   - TripalBundle $bundle
+   *       The bundle for the current entity.
    */
-  public function setTitle($entity, $title = NULL) {
-    
-    $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
-    
+  public function setTitle($entity, $title = NULL, $cache = array()) {
+
+    if (isset($cache['bundle'])) {
+      $bundle = $cache['bundle'];
+    }
+    else {
+      $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
+    }
+
     // If no title was supplied then we should try to generate one using the
     // default format set by admins.
-    if (!$title) {     
+    if (!$title) {
       $title = tripal_get_title_format($bundle);
     }
     $title = tripal_replace_entity_tokens($title, $entity, $bundle);
-    
+
     if ($title) {
       db_update('tripal_entity')
         ->fields(array(
@@ -128,25 +143,37 @@ class TripalEntityController extends EntityAPIController {
 
   /**
    * Sets the URL alias for an entity.
-   * 
+   *
    * @param $entity
    *   The entity whose URL alias should be changed.
    * @param $alias
    *   The alias to use. It can contain tokens the correspond to field values.
-   *   Token should be be compatible with those returned by 
+   *   Token should be be compatible with those returned by
    *   tripal_get_entity_tokens().
+   * @param $cache
+   *   This array is used to store objects you want to cache for performance reasons,
+   *   as well as, cache related options. The following are supported:
+   *   - TripalBundle $bundle
+   *       The bundle for the current entity.
+   *   - TripalTerm $term
+   *       The term for the current entity.
    */
-  public function setAlias($entity, $alias = NULL) {
+  public function setAlias($entity, $alias = NULL, $cache = array()) {
     $source_url = "bio_data/$entity->id";
 
     // If no alias was supplied then we should try to generate one using the
     // default format set by admins.
     if (!$alias) {
 
-      // Load the TripalBundle entity for this TripalEntity.
+      // Load the TripalBundle entity for this TripalEntity (if it's not cached).
       // First get the format for the url alias based on the bundle of the entity.
       // Then replace all the tokens with values from the entity fields.
-      $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+      if (isset($cache['bundle'])) {
+        $bundle_entity = $cache['bundle'];
+      }
+      else {
+        $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+      }
       $alias = tripal_get_bundle_variable('url_format', $bundle_entity->id);
       $alias = tripal_replace_entity_tokens($alias, $entity, $bundle_entity);
     }
@@ -155,20 +182,25 @@ class TripalEntityController extends EntityAPIController {
     // the term name and entity id.
     if (!$alias) {
 
-      // Load the term for this TripalEntity. Set a default based on the term 
-      // name and entity id. Then replace all the tokens with values from 
+      // Load the term for this TripalEntity. Set a default based on the term
+      // name and entity id. Then replace all the tokens with values from
       // the entity fields.
-      $term = entity_load('TripalTerm', array('id' => $entity->term_id));
+      $term = (isset($cache['term'])) ? $cache['term'] : entity_load('TripalTerm', array('id' => $entity->term_id));
       $term = reset($term);
       $alias = str_replace(' ', '', $term->name) . '/[TripalEntity__entity_id]';
       $alias = tripal_replace_entity_tokens($alias, $entity, $bundle_entity);
     }
-    
-    // Check if the passed alias has tokens. Load the TripalBundle entity for 
-    // this TripalEntity. Then replace all the tokens with values from the 
+
+    // Check if the passed alias has tokens. Load the TripalBundle entity for
+    // this TripalEntity. Then replace all the tokens with values from the
     // entity fields.
     if($alias && (preg_match_all("/\[[^\]]*\]/", $alias, $bundle_tokens))) {
-      $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+      if (isset($cache['bundle'])) {
+        $bundle_entity = $cache['bundle'];
+      }
+      else {
+        $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+      }
       $alias = tripal_replace_entity_tokens($alias, $entity, $bundle_entity);
     }
 
@@ -193,11 +225,13 @@ class TripalEntityController extends EntityAPIController {
         // First delete any previous alias' for this entity.
         // Then save the new one.
 
-        // TODO: publishing an entity can be very slow if there are lots of
+        // @performance: Look into this further.
+        // @spficklin publishing an entity can be very slow if there are lots of
         // entries in the url_alias table, due to this type of
-        // SQL statement that gets called somewhere by Drupal:
+        // SQL statement that gets called in drupal_path_alias_whitelist_rebuild():
         // SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM url_alias.
         // Perhaps we should write our own SQL to avoid this issue.
+        // @lacey: drupal_path_alias_whitelist_rebuild() isn't getting called for me during publish.
         $values =  array(
           'source' => $source_url,
           'alias' => $alias,
@@ -243,11 +277,16 @@ class TripalEntityController extends EntityAPIController {
           drupal_write_record('url_alias', $values);
         }
       }
-      // If there is only one alias matching then it might just be that we 
+      // If there is only one alias matching then it might just be that we
       // already assigned this alias to this entity in a previous save.
       elseif ($num_aliases == 1) {
 
-        $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+        if (isset($cache['bundle'])) {
+          $bundle_entity = $cache['bundle'];
+        }
+        else {
+          $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+        }
 
         // Check to see if the single alias is for the same entity and if not
         // warn the admin that the alias is already used (ie: not unique?)
@@ -275,7 +314,12 @@ class TripalEntityController extends EntityAPIController {
       // If there are more then one alias' matching what we generated then there's
       // a real problem and we need to warn the administrator.
       else {
-        $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+        if (isset($cache['bundle'])) {
+          $bundle_entity = $cache['bundle'];
+        }
+        else {
+          $bundle_entity = tripal_load_bundle_entity(array('name' => $entity->bundle));
+        }
 
         $aliases = db_query('SELECT source FROM {url_alias} WHERE alias=:alias',
           array(':alias' => $alias))->fetchAll();
@@ -308,19 +352,26 @@ class TripalEntityController extends EntityAPIController {
    *
    * @param $entity
    *   A TripalEntity object to save.
+   * @param $cache
+   *   This array is used to store objects you want to cache for performance reasons,
+   *   as well as, cache related options. The following are supported:
+   *   - boolean $clear_cached_fields
+   *       Clearing cached fields is NECESSARY. IF you choose to set this to false then YOU
+   *       must clear the cache yourself using cache_clear_all('field:TripalEntity:[entity_id]', 'cache_field', TRUE).
+   *       The only known reason to set this to FALSE is to clear the cache in bulk for perfomance reasons.
+   *   - TripalBundle $bundle
+   *       The bundle for the current entity.
+   *   - TripalTerm $term
+   *       The term for the current entity.
    *
    * @return
    *   The saved entity object with updated properties.
    */
-  public function save($entity, DatabaseTransaction $transaction = NULL) {
+  public function save($entity, $cache = array()) {
     global $user;
     $pkeys = array();
 
-    // Get the author information.
-    $author = $user;
-    if (property_exists($entity, 'uid')) {
-      $author = user_load($entity->uid);
-    }
+    if (!isset($cache['clear_cached_fields'])) $cache['clear_cached_fields'] = TRUE;
 
     $changed_date = time();
     $create_date = $changed_date;
@@ -338,12 +389,12 @@ class TripalEntityController extends EntityAPIController {
       }
     }
 
-    $transaction = isset($transaction) ? $transaction : db_transaction();
+    $transaction = db_transaction();
     try {
       // If our entity has no id, then we need to give it a
       // time of creation.
       if (empty($entity->id)) {
-        $entity->created = time();
+        $entity->created = $created_date;
         $invocation = 'entity_insert';
       }
       else {
@@ -363,7 +414,7 @@ class TripalEntityController extends EntityAPIController {
         'type'      => $entity->type,
         'bundle'    => $entity->bundle,
         'title'     => $entity->title,
-        'uid'       => $author->uid,
+        'uid'       => $entity->uid,
         'created'   => $create_date,
         'changed'   => $changed_date,
         'status'    => $status,
@@ -394,10 +445,10 @@ class TripalEntityController extends EntityAPIController {
       }
 
       // Set the title for this entity.
-      $this->setTitle($entity);
+      $this->setTitle($entity, NULL, $cache);
 
       // Set the path/url alias for this entity.
-      $this->setAlias($entity);
+      $this->setAlias($entity, NULL, $cache);
 
       // Invoke either hook_entity_update() or hook_entity_insert().
       module_invoke_all('entity_postsave', $entity, $entity->type);
@@ -405,8 +456,12 @@ class TripalEntityController extends EntityAPIController {
 
       // Clear any cache entries for this entity so it can be reloaded using
       // the values that were just saved.
-      $cid = 'field:TripalEntity:' . $entity->id;
-      cache_clear_all($cid, 'cache_field', TRUE);
+      // Also, we don't need to clear cached fields when publishing because we
+      // didn't attach any (see above).
+      if ($cache['clear_cached_fields'] AND ($invocation != 'entity_publish')) {
+        $cid = 'field:TripalEntity:' . $entity->id;
+        cache_clear_all($cid, 'cache_field', TRUE);
+      }
 
       return $entity;
     }
@@ -416,8 +471,6 @@ class TripalEntityController extends EntityAPIController {
       drupal_set_message("Could not save the entity: " . $e->getMessage(), "error");
       return FALSE;
     }
-
-
   }
 
   /**

+ 16 - 3
tripal/includes/TripalEntityUIController.inc

@@ -60,7 +60,7 @@ class TripalEntityUIController extends EntityDefaultUIController {
     }
 
     // Link for viewing a tripal data type.
-    $items['bio_data/' . $wildcard] = array(
+    $items['bio_data/%'] = array(
       'title callback' => 'tripal_entity_title',
       'title arguments' => array(1),
       'page callback' => 'tripal_view_entity',
@@ -71,7 +71,7 @@ class TripalEntityUIController extends EntityDefaultUIController {
     );
 
     // 'View' tab for an individual entity page.
-    $items['bio_data/' . $wildcard . '/view'] = array(
+    $items['bio_data/%/view'] = array(
       'title' => 'View',
       'page callback' => 'tripal_view_entity',
       'page arguments' => array(1),
@@ -115,7 +115,20 @@ class TripalEntityUIController extends EntityDefaultUIController {
  * @see hook_entity_view_alter()
  */
 function tripal_view_entity($entity, $view_mode = 'full') {
-   $content = '';
+   if(!is_object($entity)) {
+     $id = intval($entity);
+     if($id === 0) {
+       return drupal_not_found();
+     }
+
+     $entities = tripal_load_entity('TripalEntity', [$id]);
+     if(empty($entities)) {
+       return drupal_not_found();
+     }
+
+     $entity = reset($entities);
+   }
+
    $controller = entity_get_controller($entity->type);
    $content = $controller->view(array($entity->id => $entity));
    drupal_set_title($entity->title);

+ 0 - 7
tripal/includes/TripalFieldQuery.inc

@@ -120,13 +120,6 @@ class TripalFieldQuery extends EntityFieldQuery {
       $results = $this->_intersectResults($results, $st_results);
     }
 
-    // If this is a count query then it should return a numeric value.
-    if ($this->count) {
-      if (!$results or !is_array($results)) {
-        return 0;
-      }
-      return count($results['TripalEntity']);
-    }
     return $results;
   }
   

+ 34 - 6
tripal/includes/tripal.entity.inc

@@ -432,20 +432,48 @@ function tripal_field_property_get($entity, array $options, $field_name, $entity
  */
 function tripal_entity_access($op, $entity = NULL, $account = NULL, $entity_type = NULL) {
   global $user;
+  $cache = &drupal_static(__FUNCTION__, NULL);
 
-  if ($entity) {
+  if (!isset($account)) {
+    $account = $user;
+  }
+
+  if (is_object($entity)) {
     $bundle_name = $entity->bundle;
   }
+  elseif (intval($entity) !== 0) {
+    if (!isset($cache)) {
+      $cache = cache_get("tripal_entity_access_cache");
+      if (isset($cache->data)) {
+        $cache = $cache->data;
+      }
+    }
+
+    if (empty($cache)) {
+      $cache = [];
+    }
+
+    if (isset($cache[$entity])) {
+      $bundle_name = $cache[$entity];
+    }
+    else {
+      $sql = 'SELECT {bundle} FROM tripal_entity WHERE id = :id';
+      $bundle_name = db_query($sql, [':id' => $entity])->fetchField();
+      $cache[$entity] = $bundle_name;
+      cache_set("tripal_entity_access_cache", $cache);
+    }
+  }
   else {
     return FALSE;
   }
 
-  if (!isset($account)) {
-    $account = $user;
-  }
-
   if (!$entity_type) {
-    $entity_type = $entity->type;
+    if (is_object($entity)) {
+      $entity_type = $entity->type;
+    }
+    else {
+      $entity_type = 'TripalEntity';
+    }
   }
 
   // See if other modules want to adust permissions.

+ 116 - 72
tripal_chado/api/tripal_chado.api.inc

@@ -4,7 +4,7 @@
  * @file
  *
  * This file contains miscellaneous API functions specific to working with
- * records in Chado that do not have a home in any other sub category of 
+ * records in Chado that do not have a home in any other sub category of
  * API functions.
  */
 
@@ -12,9 +12,9 @@
  * @defgroup tripal_chado_api Chado
  *
  * @ingroup tripal_api
- * The Tripal Chado API is a set of functions for interacting with data 
+ * The Tripal Chado API is a set of functions for interacting with data
  * inside of a Chado relational database. Entities (or pages) in Drupal
- * that are provided by Tripal can supply data from any supported database 
+ * that are provided by Tripal can supply data from any supported database
  * back-end, and Chado is the default. This API contains a variety of sub
  * categories (or groups) where functions are organized.  Any extension module
  * that desires to work with data in Chado will find these functions useful.
@@ -39,6 +39,9 @@
  */
 function chado_publish_records($values, $job_id = NULL) {
 
+  // Used for adding runtime to the progress report.
+  $started_at = microtime(true);
+
   // We want the job object in order to report progress.
   if (is_object($job_id)) {
     $job = $job_id;
@@ -52,6 +55,9 @@ function chado_publish_records($values, $job_id = NULL) {
     $report_progress = TRUE;
   }
 
+  // Start an array for caching objects to save performance.
+  $cache = array();
+
   // Make sure we have the required options: bundle_name.
   if (!array_key_exists('bundle_name', $values) or !$values['bundle_name']) {
     tripal_report_error('tripal_chado', TRIPAL_ERROR,
@@ -65,9 +71,15 @@ function chado_publish_records($values, $job_id = NULL) {
   $filters = array_key_exists('filters', $values) ? $values['filters'] : array();
   $sync_node = array_key_exists('sync_node', $values) ? $values['sync_node'] : '';
 
+  // We want to break the number of records to publish into chunks in order to ensure
+  // transactions do not run for too long (performance issue). The number of records
+  // to be processed per chunk is set here:
+  $chunk_size = 500;
+
   // Load the bundle entity so we can get information about which Chado
   // table/field this entity belongs to.
   $bundle = tripal_load_bundle_entity(array('name' => $bundle_name));
+  $cache['bundle'] = $bundle;
   if (!$bundle) {
     tripal_report_error('tripal_chado', TRIPAL_ERROR,
         "Unknown bundle. Could not publish record: @error",
@@ -76,7 +88,6 @@ function chado_publish_records($values, $job_id = NULL) {
   }
   $chado_entity_table = chado_get_bundle_entity_table($bundle);
 
-
   // Get the mapping of the bio data type to the Chado table.
   $chado_bundle = db_select('chado_bundle', 'cb')
     ->fields('cb')
@@ -89,6 +100,10 @@ function chado_publish_records($values, $job_id = NULL) {
     return FALSE;
   }
 
+  // Load the term for use in setting the alias for each entity created.
+  $term = entity_load('TripalTerm', array('id' => $entity->term_id));
+  $cache['term'] = $term;
+
   $table = $chado_bundle->data_table;
   $type_column = $chado_bundle->type_column;
   $type_linker_table = $chado_bundle->type_linker_table;
@@ -181,86 +196,115 @@ function chado_publish_records($values, $job_id = NULL) {
       }
     }
   }
+
   // First get the count
+  // @performance optimize, estimate or remove this. It's only used for reporting progress on the command-line.
   $sql = "SELECT count(*) as num_records " . $from . $where;
   $result = chado_query($sql, $args);
   $count = $result->fetchField();
+  print "\nThere are $count records to publish.\n";
 
-  // calculate the interval for updates
-  $interval = intval($count / 50);
-  if ($interval < 1) {
-    $interval = 1;
-  }
+  print "\nNOTE: publishing records is performed using database transactions. If the job fails\n" .
+          "or is terminated prematurely then the current set of $chunk_size is rolled back with\n" .
+          "no changes to the database. Simply re-run the publishing job to publish any remaining\n".
+          "content after fixing the issue that caused the job to fail.\n\n" .
+          "Also, the following progress only updates every $chunk_size records.\n";
 
   // Perform the query.
-  $sql = $select . $from . $where;
-  $records = chado_query($sql, $args);
-  $transaction = db_transaction();
-
-  print "\nNOTE: publishing records is performed using a database transaction. \n" .
-      "If the load fails or is terminated prematurely then the entire set of \n" .
-      "is rolled back with no changes to the database\n\n";
-
-  $i = 0;
-  printf("%d of %d records. (%0.2f%%) Memory: %s bytes\r", $i, $count, 0, number_format(memory_get_usage()));
-  try {
-    while($record = $records->fetchObject()) {
-
-      // update the job status every interval
-      if ($i % $interval == 0) {
-        $complete = ($i / $count) * 33.33333333;
-        // Currently don't support setting job progress within a transaction.
-        // if ($report_progress) { $job->setProgress(intval($complete * 3)); }
-        printf("%d of %d records. (%0.2f%%) Memory: %s bytes\r", $i, $count, $complete * 3, number_format(memory_get_usage()));
-      }
+  $sql = $select . $from . $where . ' LIMIT '.$chunk_size;
+  $more_records_to_publish = TRUE;
+  $total_published = 0;
+  while ($more_records_to_publish) {
+
+    $records = chado_query($sql, $args);
+
+    // Update the job status every chunk start.
+    // Because this is outside of hte transaction, we can update the admin through the jobs UI.
+    $complete = ($total_published / $count) * 33.33333333;
+    if ($report_progress) { $job->setProgress(intval($complete * 3)); }
+    if ($total_published === 0) {
+      printf("%d of %d records. (%0.2f%%) Memory: %s bytes.\r",
+        $i, $count, 0, number_format(memory_get_usage()), 0);
+    }
+    else {
+      printf("%d of %d records. (%0.2f%%) Memory: %s bytes; Current run time: %s minutes.\r",
+        $total_published, $count, $complete * 3, number_format(memory_get_usage()), number_format((microtime(true) - $started_at)/60, 2));
+    }
 
-      // First save the tripal_entity record.
-      $record_id = $record->record_id;
-      $ec = entity_get_controller('TripalEntity');
-      $entity = $ec->create(array(
-        'bundle' => $bundle_name,
-        'term_id' => $bundle->term_id,
-        // Add in the Chado details for when the hook_entity_create()
-        // is called and our tripal_chado_entity_create() implementation
-        // can deal with it.
-        'chado_record' => chado_generate_var($table, array($pkey_field => $record_id)),
-        'chado_record_id' => $record_id,
-        'publish' => TRUE,
-      ));
-      $entity = $entity->save();
-      if (!$entity) {
-        throw new Exception('Could not create entity.');
-      }
+    // There is no need to cache transactions since Drupal handles nested transactions
+    // "by performing no transactional operations (as far as the database sees) within
+    // the inner nesting layers". Effectively, Drupal ensures nested trasactions work the
+    // same as passing a transaction through to the deepest level and not starting a new
+    // transaction if we are already in one.
+    $transaction = db_transaction();
+
+    try {
+      $i = 0;
+      while($record = $records->fetchObject()) {
+
+        // First save the tripal_entity record.
+        // @performace This is likely a bottleneck. Too bad we can't create
+        // multiple entities at once... sort of like the copy method.
+        $record_id = $record->record_id;
+        $ec = entity_get_controller('TripalEntity');
+
+        $entity = $ec->create(array(
+          'bundle' => $bundle_name,
+          'term_id' => $bundle->term_id,
+          // Add in the Chado details for when the hook_entity_create()
+          // is called and our tripal_chado_entity_create() implementation
+          // can deal with it.
+          'chado_record' => chado_generate_var($table, array($pkey_field => $record_id), array('include_fk' => 0)),
+          'chado_record_id' => $record_id,
+          'publish' => TRUE,
+          'bundle_object' => $bundle,
+        ));
+
+        $entity = $entity->save($cache);
+        if (!$entity) {
+          throw new Exception('Could not create entity.');
+        }
 
-      // Next save the chado entity record.
-      $entity_record = array(
-        'entity_id' => $entity->id,
-        'record_id' => $record_id,
-      );
+        // Next save the chado entity record.
+        $entity_record = array(
+          'entity_id' => $entity->id,
+          'record_id' => $record_id,
+        );
 
-      // For the Tv2 to Tv3 migration we want to add the nid to the
-      // entity so we can associate the node with the entity.
-      if (property_exists($record, 'nid')) {
-        $entity_record['nid'] = $record->nid;
-      }
-      $result = db_insert($chado_entity_table)
-        ->fields($entity_record)
-        ->execute();
-      if(!$result){
-        throw new Exception('Could not create mapping of entity to Chado record.');
+        // For the Tv2 to Tv3 migration we want to add the nid to the
+        // entity so we can associate the node with the entity.
+        if (property_exists($record, 'nid')) {
+          $entity_record['nid'] = $record->nid;
+        }
+        $result = db_insert($chado_entity_table)
+          ->fields($entity_record)
+          ->execute();
+        if(!$result){
+          throw new Exception('Could not create mapping of entity to Chado record.');
+        }
+
+        $i++;
+        $total_published++;
       }
+    }
+    catch (Exception $e) {
+      $transaction->rollback();
+      $error = $e->getMessage();
+      tripal_report_error('tripal_chado', TRIPAL_ERROR, "Could not publish record: @error", array('@error' => $error));
+      drupal_set_message('Failed publishing record. See recent logs for more details.', 'error');
+      return FALSE;
+    }
 
-      $i++;
+    // If we get through the loop and haven't completed 100 records, then we're done!
+    if ($i < $chunk_size) {
+      $more_records_to_publish = FALSE;
     }
+
+    // Commit our current chunk.
+    unset($transaction);
   }
-  catch (Exception $e) {
-    $transaction->rollback();
-    $error = $e->getMessage();
-    tripal_report_error('tripal_chado', TRIPAL_ERROR, "Could not publish record: @error", array('@error' => $error));
-    drupal_set_message('Failed publishing record. See recent logs for more details.', 'error');
-    return FALSE;
-  }
-  drupal_set_message("Succesfully published $i " . $bundle->label . " record(s).");
+
+  drupal_set_message("Succesfully published $total_published " . $bundle->label . " record(s).");
   return TRUE;
 }
 
@@ -271,7 +315,7 @@ function chado_publish_records($values, $job_id = NULL) {
  *    The name of a base table in Chado.
  * @return
  *    An array of tokens where the key is the machine_name of the token.
- * 
+ *
  * @ingroup tripal_chado_api
  */
 function chado_get_tokens($base_table) {
@@ -329,7 +373,7 @@ function chado_get_tokens($base_table) {
  *
  * @return
  *   The string will all tokens replaced with values.
- * 
+ *
  *  @ingroup tripal_chado_api
  */
 function chado_replace_tokens($string, $record) {

+ 1 - 1
tripal_chado/api/tripal_chado.query.api.inc

@@ -1671,7 +1671,7 @@ function chado_query($sql, $args = array()) {
   // names with 'chado'.
   if ($is_local) {
     // Remove carriage returns from the SQL.
-    $sql = preg_replace('/\n/', '', $sql);
+    $sql = preg_replace('/\n/', ' ', $sql);
 
     // Prefix the tables with their correct schema.
     // Chado tables should be enclosed in curly brackets (ie: {feature} )

+ 3 - 2
tripal_chado/api/tripal_chado.variables.api.inc

@@ -621,10 +621,11 @@ function chado_expand_var($object, $type, $to_expand, $table_options = array())
 
   // Make sure we have a value.
   if (!$object) {
+    $trace = (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
     tripal_report_error('tripal_chado',
       TRIPAL_ERROR,
-      'Cannot pass non array as argument, $object, to chado_expand_var function.',
-      array());
+      'Missing $object argument to the chado_expand_var function from %caller (line: %line)',
+      array('%caller' => $trace[1]['function'], '%line' => $trace[0]['line']));
     return $object;
   }
 

+ 5 - 0
tripal_chado/includes/TripalFields/chado_linker__contact/chado_linker__contact.inc

@@ -137,6 +137,11 @@ class chado_linker__contact extends ChadoField {
     $fkey_lcolumn = key($schema['foreign keys'][$base_table]['columns']);
     $fkey_rcolumn = $schema['foreign keys'][$base_table]['columns'][$fkey_lcolumn];
 
+    // If we don't have a chado record return before creating a stub for this field!
+    if (!$record) {
+      return;
+    }
+    
     // Set some defaults for the empty record.
     $entity->{$field_name}['und'][0] = array(
       'value' => array(),

+ 5 - 0
tripal_chado/includes/TripalFields/sbo__database_cross_reference/sbo__database_cross_reference.inc

@@ -129,6 +129,11 @@ class sbo__database_cross_reference extends ChadoField {
     $accession_term = chado_get_semweb_term('dbxref', 'accession');
     $dburl_term = chado_get_semweb_term('db', 'url');
 
+    // If we don't have a chado record return before creating a stub for this field!
+    if (!$record) {
+      return;
+    }
+    
     // Set some defaults for the empty record.
     $entity->{$field_name}['und'][0] = array(
       'value' => '',

+ 17 - 2
tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship.inc

@@ -304,12 +304,20 @@ class sbo__relationship extends ChadoField {
 
     // 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;;
+      $entity->{$field_name}['und'][$delta]['value']['local:relationship_subject']['data:0842'] = $relationship->$subject_id_key->uniquename;
     }
     if (property_exists($relationship->$object_id_key, 'uniquename')) {
       $entity->{$field_name}['und'][$delta]['value']['local:relationship_object']['data:0842'] = $relationship->$object_id_key->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;
+    }
+    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;
+    }
+
     // Add in the TripalEntity ids if these base records in the relationship
     // are published.
     if (property_exists($relationship->$subject_id_key, 'entity_id')) {
@@ -376,6 +384,11 @@ class sbo__relationship extends ChadoField {
     $fkey_lcolumn = key($schema['foreign keys'][$base_table]['columns']);
     $fkey_rcolumn = $schema['foreign keys'][$base_table]['columns'][$fkey_lcolumn];
 
+    // 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'];
@@ -430,9 +443,11 @@ class sbo__relationship extends ChadoField {
         'type_id' => 1,
         $object_id_key => array(
           'type_id' => 1,
+          'organism_id' => 1,
         ),
         $subject_id_key  => array(
           'type_id' => 1,
+          'organism_id' => 1,
         ),
       ),
     );
@@ -974,4 +989,4 @@ class sbo__relationship extends ChadoField {
   public function queryOrder($query, $order) {
 
   }
-}
+}

+ 1 - 1
tripal_chado/includes/TripalFields/sbo__relationship/sbo__relationship_formatter.inc

@@ -3,7 +3,7 @@
 class sbo__relationship_formatter extends ChadoFieldFormatter {
 
   // The default lable for this field.
-  public static $default_label = 'Relationship';
+  public static $default_label = 'Relationship Statements';
 
   // The list of field types for which this formatter is appropriate.
   public static $field_types = array('sbo__relationship');

+ 223 - 0
tripal_chado/includes/TripalFields/sbo__relationship_table/sbo__relationship_table_formatter.inc

@@ -0,0 +1,223 @@
+<?php
+
+class sbo__relationship_table_formatter extends ChadoFieldFormatter {
+
+  // The default lable for this field.
+  public static $default_label = 'Relationship Table by Type';
+
+  // The list of field types for which this formatter is appropriate.
+  public static $field_types = array('sbo__relationship');
+
+  public static $default_settings = array(
+    'subject_caption' => 'This @content_type is <em>@rel_type</em> the following:',
+    'object_caption' => 'The following are <em>@rel_type</em> this @content_type:',
+  );
+
+  /**
+   *
+   * @see TripalFieldFormatter::settingsForm()
+   */
+  public function settingsForm($view_mode, $form, &$form_state) {
+
+    $display = $this->instance['display'][$view_mode];
+    $settings = $display['settings'];
+
+    // Ensure the default are set if the value is not configured.
+    foreach ($this::$default_settings as $key => $value) {
+      if (!isset($settings[$key])) { $settings[$key] = $value; }
+    }
+
+    $element = array();
+    $element['subject_caption'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Caption: where current entity is the subject.',
+      '#description' => 'This labels the relationship tables where the current entity is the subject of the relationship.',
+      '#default_value' => $settings['subject_caption'],
+    );
+
+    $element['object_caption'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Caption: where current entity is the object.',
+      '#description' => 'This labels the relationship tables where the current entity is the object of the relationship.',
+      '#default_value' => $settings['object_caption'],
+    );
+
+    $element['tokens'] = array(
+      '#type' => 'item',
+      '#title' => 'Tokens',
+      '#markup' => 'The following tokens should be used in <strong>both the above captions</strong>:</p>
+                     <ul>
+                       <li>@rel_type: the type name of the current relationship.</li>
+                       <li>@content_type: the human-readable name of the content type for the current entity.</li>
+                     </ul>',
+    );
+
+    return $element;
+  }
+
+  /**
+   * @see TripalFieldFormatter::settingsSummary()
+   */
+  public function settingsSummary($view_mode) {
+    $display = $this->instance['display'][$view_mode];
+    $settings = $display['settings'];
+
+    // Ensure the default are set if the value is not configured.
+    foreach ($this::$default_settings as $key => $value) {
+      if (!isset($settings[$key])) { $settings[$key] = $value; }
+    }
+
+    $summary = t('<strong>Subject Caption:</strong> @subject<br><strong>Object Caption:</strong> @object',
+        array(
+          '@subject' => $settings['subject_caption'],
+          '@object' => $settings['object_caption'])
+        );
+
+    return $summary;
+  }
+
+  /**
+   *
+   * @see TripalFieldFormatter::view()
+   */
+  public function view(&$element, $entity_type, $entity, $langcode, $items, $display) {
+
+    // Get the settings and set defaults.
+    $settings = $display['settings'];
+    foreach ($this::$default_settings as $key => $value) {
+      if (!isset($settings[$key])) { $settings[$key] = $value; }
+    }
+
+    // Headers depending on which fields are available.
+    $headers = array(
+      'all' => array('Name', 'Unique Name', 'Species', 'Type'),
+      'nst' => array('Name', 'Species', 'Type'),
+      'nut' => array('Name', 'Unique Name', 'Type'),
+      'nt' => array('Name', 'Type'),
+    );
+
+    // This is an array of tables where each table corresponds to a relationship type.
+    // The header is the same for all tables and the caption includes a sentence
+    // stating the relationship type.
+    $tables = array();
+
+    // For each relationship...
+    foreach ($items as $delta => $item) {
+      if (empty($item['value'])) {
+        continue;
+      }
+
+      $subject_name = $item['value']['local:relationship_subject']['schema:name'];
+      $subject_uniquename = (isset($item['value']['local:relationship_subject']['data:0842'])) ? $item['value']['local:relationship_subject']['data:0842'] : NULL;
+      $subject_type = $item['value']['local:relationship_subject']['rdfs:type'];
+      $subject_species = (isset($item['value']['local:relationship_subject']['OBI:0100026'])) ? $item['value']['local:relationship_subject']['OBI:0100026'] : NULL;
+
+      $object_name = $item['value']['local:relationship_object']['schema:name'];
+      $object_uniquename = (isset($item['value']['local:relationship_object']['data:0842'])) ? $item['value']['local:relationship_object']['data:0842'] : NULL;
+      $object_type = $item['value']['local:relationship_object']['rdfs:type'];
+      $object_species = (isset($item['value']['local:relationship_object']['OBI:0100026'])) ? $item['value']['local:relationship_object']['OBI:0100026'] : NULL;
+
+      $relationship_type = $item['value']['local:relationship_type'];
+
+      // Make types more readable.
+      foreach(array('subject_type', 'object_type', 'relationship_type') as $var) {
+        $$var = ucwords(str_replace('_',' ', $$var));
+      }
+
+      // Handle some special cases.
+      // For mRNA objects we don't want to show the CDS, exons, 5' UTR, etc.
+      // we want to show the parent gene and the protein.
+      if ($object_type == 'mRNA' and
+          (in_array($subject_type, array('CDS', 'exon', 'five_prime_UTR', 'three_prime_UTR')))) {
+        continue;
+      }
+
+      // Set header type based on what values are available.
+      if (!isset($tables[$relationship_type])) {
+        $tables[$relationship_type]['header_type'] = 'all';
+        if ($subject_uniquename === NULL AND $subject_species === NULL) {
+          $tables[$relationship_type]['header_type'] = 'nt';
+        }
+        elseif ($subject_uniquename === NULL) {
+          $tables[$relationship_type]['header_type'] = 'nst';
+        }
+        elseif ($subject_species === NULL) {
+          $tables[$relationship_type]['header_type'] = 'nut';
+        }
+      }
+
+      // Convert the object/subject to a link if an entity exists for it.
+      if (array_key_exists('entity', $item['value']['local:relationship_object'])) {
+        list($entity_type, $object_entity_id) = explode(':', $item['value']['local:relationship_object']['entity']);
+        if ($object_entity_id != $entity->id) {
+          $object_name = l($object_name, 'bio_data/' . $object_entity_id);
+          if ($object_uniquename) { $object_uniquename = l($object_uniquename, 'bio_data/' . $object_entity_id); }
+        }
+      }
+      if (array_key_exists('entity', $item['value']['local:relationship_subject'])) {
+        list($entity_type, $subject_entity_id) = explode(':', $item['value']['local:relationship_subject']['entity']);
+        if ($subject_entity_id != $entity->id) {
+          $subject_name = l($subject_name, 'bio_data/' . $subject_entity_id);
+          if ($subject_uniquename) { $subject_uniquename = l($subject_uniquename, 'bio_data/' . $subject_entity_id); }
+        }
+      }
+
+      // Determine if the subject or object is the current entity.
+      $is_subject = TRUE;
+      if ($object_name == $entity->schema__name['und'][0]['value']) {
+        $is_subject = FALSE;
+      }
+
+
+      // Add the related entity to the tables array by type of relationship.
+      if ($is_subject) {
+        $tables[$relationship_type]['caption'] = t($settings['subject_caption'],
+          array('@rel_type' => $relationship_type, '@content_type' => $entity->rdfs__type['und'][0]['value']));
+
+        $row = array();
+        $row['name'] = $object_name;
+        if ($object_uniquename) { $row['uniquename'] = $object_uniquename; }
+        if ($object_species) { $row['species'] = $object_species; }
+        $row['type'] = $object_type;
+
+        $tables[$relationship_type]['rows'][] = $row;
+      }
+      else {
+        $tables[$relationship_type]['caption'] = t($settings['object_caption'],
+          array('@rel_type' => $relationship_type, '@content_type' => $entity->rdfs__type['und'][0]['value']));
+
+        $row = array();
+        $row['name'] = $subject_name;
+        if ($subject_uniquename) { $row['uniquename'] = $subject_uniquename; }
+        if ($subject_species) { $row['species'] = $subject_species; }
+        $row['type'] = $subject_type;
+
+        $tables[$relationship_type]['rows'][] = $row;
+      }
+    }
+
+    $element[0] = array(
+      '#type' => 'markup',
+      '#tree' => TRUE,
+    );
+
+    foreach ($tables as $relationship_type => $details) {
+
+      $table_id = 'sbo__relationship-'.$item['value']['local:relationship_subject']['rdfs:type']; //$this->getPagerElementID();
+      $element[0][$relationship_type] = array(
+        '#type' => 'markup',
+        '#theme' => 'table',
+        '#header' => $headers[ $details['header_type'] ],
+        '#rows' => $details['rows'],
+        '#caption' => $details['caption'],
+        '#sticky' => FALSE,
+        '#attributes' => array(
+          'class' => 'tripal-data-table',
+          'id' => $table_id,
+        ),
+      );
+    }
+  }
+}
+
+

+ 2 - 2
tripal_chado/includes/TripalFields/schema__publication/schema__publication_widget.inc

@@ -151,8 +151,8 @@ class schema__publication_widget extends ChadoFieldWidget {
     // write a record.
     if (!$title) {
       $form_state['values'][$field_name]['und'][$delta]['value'] = 'delete_me';
-      $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__' . $fkey] = '';
-      $form_state['values'][$field_name]['und'][$delta]['chado-' . $field_table . '__pub_id'] = '';
+      $form_state['values'][$field_name]['und'][$delta][$pub_item_id] = '';
+      $form_state['values'][$field_name]['und'][$delta]['value'] = '';
     }
   }
 

+ 10 - 5
tripal_chado/includes/TripalFields/sep__protocol/sep__protocol.inc

@@ -117,16 +117,21 @@ class sep__protocol extends ChadoField {
     $base_table = $this->instance['settings']['base_table'];
     $linker_field = 'chado-' . $field_table . '__' . $field_column;
 
-    // Set some defaults for the empty record.
-    $entity->{$field_name}['und'][0] = [
-      'value' => [],
-    ];
-
     if (!$record) {
       return;
     }
     
     $protocol = $record->{$field_column};
+    // If this record does not have a protcol then just return.
+    if (!$protocol) {
+      return;
+    }
+    
+    // Set some defaults for the empty record.
+    $entity->{$field_name}['und'][0] = [
+      'value' => [],
+    ];
+    
     $entity_id = $record->entity_id;
 
     // Get the Protocol controlled vocabulary terms.

+ 11 - 10
tripal_chado/includes/TripalFields/sep__protocol/sep__protocol_formatter.inc

@@ -32,24 +32,25 @@ class sep__protocol_formatter extends ChadoFieldFormatter {
    */
   public function view(&$element, $entity_type, $entity, $langcode, $items, $display) {
 
-    if (count($items) > 0) {
+    if ($items[0]['value']) {
       
       $protocol_name_term = chado_get_semweb_term('protocol', 'name');
       $protocol_type_term = chado_get_semweb_term('protocol', 'type_id');    
 
       $protocol_name =  $items[0]['value'][$protocol_name_term];
       $content = $protocol_name;
-      list($entity_type, $entity_id) = explode(':', $items[0]['value']['entity_id']);
-      if ($entity_id) {
-        $content = l($protocol_name, 'bio_data/' . $entity_id);
+      if (array_key_exists('entity_id', $items[0]['value'])) {
+        list($entity_type, $entity_id) = explode(':', $items[0]['value']['entity_id']);
+        if ($entity_id) {
+          $content = l($protocol_name, 'bio_data/' . $entity_id);
+        }
       }
+      //cardinality for this field is 1
+      $element[0] = [
+        '#type' => 'markup',
+        '#markup' => $content,
+      ];
     }
-
-    //cardinality for this field is 1
-    $element[0] = [
-      '#type' => 'markup',
-      '#markup' => $content,
-    ];
   }
 
 

+ 8 - 54
tripal_chado/includes/TripalFields/sep__protocol/sep__protocol_widget.inc

@@ -16,59 +16,7 @@ class sep__protocol_widget extends ChadoFieldWidget {
   public static $field_types = ['sep__protocol'];
 
   /**
-   * Provides the form for editing of this field.
-   *
-   * This function corresponds to the hook_field_widget_form()
-   * function of the Drupal Field API.
-   *
-   * This form is diplayed when the user creates a new entity or edits an
-   * existing entity.  If the field is attached to the entity then the form
-   * provided by this function will be displayed.
-   *
-   * At a minimum, the form must have a 'value' element.  For Tripal, the
-   * 'value' element of a field always corresponds to the value that is
-   * presented to the end-user either directly on the page (with formatting)
-   * or via web services, or some other mechanism.  However, the 'value' is
-   * sometimes not enough for a field.  For example, the Tripal Chado module
-   * maps fields to table columns and sometimes those columns are foreign keys
-   * therefore, the Tripal Chado modules does not just use the 'value' but adds
-   * additional elements to help link records via FKs.  But even in this case
-   * the 'value' element must always be present in the return form and in such
-   * cases it's value should be set equal to that added in the 'load' function.
-   *
-   * @param $widget
-   * @param $form
-   *   The form structure where widgets are being attached to. This might be a
-   *   full form structure, or a sub-element of a larger form.
-   * @param $form_state
-   *   An associative array containing the current state of the form.
-   * @param $langcode
-   *   The language associated with $items.
-   * @param $items
-   *   Array of default values for this field.
-   * @param $delta
-   *   The order of this item in the array of subelements (0, 1, 2, etc).
-   * @param $element
-   * A form element array containing basic properties for the widget:
-   *  - #entity_type: The name of the entity the field is attached to.
-   *  - #bundle: The name of the field bundle the field is contained in.
-   *  - #field_name: The name of the field.
-   *  - #language: The language the field is being edited in.
-   *  - #field_parents: The 'parents' space for the field in the form. Most
-   *    widgets can simply overlook this property. This identifies the location
-   *    where the field values are placed within $form_state['values'], and is
-   *    used to access processing information for the field through the
-   *    field_form_get_state() and field_form_set_state() functions.
-   *  - #columns: A list of field storage columns of the field.
-   *  - #title: The sanitized element label for the field instance, ready for
-   *    output.
-   *  - #description: The sanitized element description for the field instance,
-   *    ready for output.
-   *  - #required: A Boolean indicating whether the element value is required;
-   *    for required multiple value fields, only the first widget's values are
-   *    required.
-   *  - #delta: The order of this item in the array of subelements; see
-   *    $delta above
+   * @see TripalFieldWidget::form()
    */
   public function form(&$widget, &$form, &$form_state, $langcode, $items, $delta, $element) {
     parent::form($widget, $form, $form_state, $langcode, $items, $delta, $element);
@@ -137,7 +85,13 @@ class sep__protocol_widget extends ChadoFieldWidget {
     
     // Make sure the value is set to the organism_id
     $protocol_id = $form_state['values'][$field_name]['und'][0][$linker_field];
-    $form_state['values'][$field_name]['und'][0]['value'] = $protocol_id;
+    if ($protocol_id) {
+      $form_state['values'][$field_name]['und'][0]['value'] = $protocol_id;
+    }
+    else {
+      $form_state['values'][$field_name]['und'][0]['value'] = '__NULL__';
+      $form_state['values'][$field_name]['und'][0][$linker_field] = '__NULL__';
+    }
   }
 
   /**

+ 11 - 2
tripal_chado/includes/tripal_chado.entity.inc

@@ -7,8 +7,15 @@
  * This hook is called when brand new entities are created, but
  * they are not loaded so the hook_entity_load() is not yet called. We
  * can use this hook to add properties to the entity before saving.
+ *
+ * @param $entity
+ *   The entity being created.
+ * @param $type
+ *   The type of entity being created.
+ * @param $bundle (OPTIONAL)
+ *   The bundle object for the current entity.
  */
-function tripal_chado_entity_create(&$entity, $type) {
+function tripal_chado_entity_create(&$entity, $type, $bundle = NULL) {
   if ($type == 'TripalEntity') {
 
     // Set some defaults on vars needed by this module.
@@ -18,7 +25,9 @@ function tripal_chado_entity_create(&$entity, $type) {
       $entity->chado_linker = NULL;
 
       // Add in the Chado table information for this entity type.
-      $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
+      if (!$bundle) {
+        $bundle = tripal_load_bundle_entity(array('name' => $entity->bundle));
+      }
       if ($bundle->data_table) {
         $entity->chado_table = $bundle->data_table;
         $entity->chado_column = $bundle->type_column;