Ver código fonte

Merge branch '7.x-3.x' into revert-663-revert-628-541-tv3-ajax_fields

Stephen Ficklin 6 anos atrás
pai
commit
190ce9be68
56 arquivos alterados com 4705 adições e 1601 exclusões
  1. 1 0
      docs/dev_guide.rst
  2. 296 0
      docs/dev_guide/chado.rst
  3. 1 1
      docs/dev_guide/tutorials.rst
  4. 1 1
      docs/user_guide.rst
  5. 11 4
      docs/user_guide/bulk_loader.rst
  6. 8 3
      docs/user_guide/example_genomics/pub_import.rst
  7. 1 0
      docs/user_guide/install_tripal.rst
  8. 7 3
      docs/user_guide/install_tripal/automating_job_execution.rst
  9. 13 0
      docs/user_guide/install_tripal/drupal_home.rst
  10. 25 20
      docs/user_guide/install_tripal/manual_install/install_drupal.rst
  11. 8 3
      docs/user_guide/install_tripal/manual_install/install_prereqs.rst
  12. 11 6
      docs/user_guide/install_tripal/manual_install/install_tripal.rst
  13. 13 6
      docs/user_guide/install_tripal/rapid_install.rst
  14. 3 3
      docs/user_guide/install_tripal/upgrade_from_tripal2.rst
  15. 6 1
      docs/user_guide/job_management.rst
  16. 5 1
      docs/user_guide/mviews.rst
  17. 5 1
      docs/user_guide/web_services.rst
  18. 137 0
      tests/tripal_chado/api/ChadoComplianceTest.php
  19. 313 0
      tests/tripal_chado/api/ChadoSchemaTest.php
  20. 119 0
      tests/tripal_chado/example_files/test.obo
  21. 126 0
      tests/tripal_chado/example_files/test.update.obo
  22. 3 2
      tests/tripal_chado/loaders/GFF3ImporterTest.php
  23. 606 0
      tests/tripal_chado/loaders/OBOImporterTest.php
  24. 4 4
      tripal/api/tripal.DEPRECATED.api.inc
  25. 0 2
      tripal/api/tripal.notice.api.inc
  26. 0 2
      tripal/api/tripal.terms.api.inc
  27. 5 1
      tripal/includes/TripalFieldDownloaders/TripalCSVDownloader.inc
  28. 5 1
      tripal/includes/TripalFieldDownloaders/TripalTabDownloader.inc
  29. 19 14
      tripal/includes/TripalImporter.inc
  30. 38 14
      tripal/includes/TripalJob.inc
  31. 2 2
      tripal/includes/tripal.jobs.inc
  32. 140 107
      tripal/includes/tripal.term_lookup.inc
  33. 79 37
      tripal_chado/api/ChadoRecord.inc
  34. 629 0
      tripal_chado/api/ChadoSchema.inc
  35. 270 474
      tripal_chado/api/modules/tripal_chado.cv.api.inc
  36. 15 16
      tripal_chado/api/modules/tripal_chado.feature.api.inc
  37. 1 3
      tripal_chado/api/modules/tripal_chado.module.DEPRECATED.api.inc
  38. 1 1
      tripal_chado/api/tripal_chado.mviews.api.inc
  39. 27 3
      tripal_chado/api/tripal_chado.schema_v1.2.api.inc
  40. 19 17
      tripal_chado/api/tripal_chado.schema_v1.3.api.inc
  41. 1 0
      tripal_chado/api/tripal_chado.variables.api.inc
  42. 53 52
      tripal_chado/files/tcontact.obo
  43. 1 2
      tripal_chado/files/tpub.obo
  44. 1306 689
      tripal_chado/includes/TripalImporter/OBOImporter.inc
  45. 16 15
      tripal_chado/includes/setup/tripal_chado.chado_vx_x.inc
  46. 39 0
      tripal_chado/includes/setup/tripal_chado.setup.inc
  47. 4 1
      tripal_chado/includes/tripal_chado.bundle.inc
  48. 6 2
      tripal_chado/includes/tripal_chado.entity.inc
  49. 1 1
      tripal_chado/includes/tripal_chado.fields.inc
  50. 2 7
      tripal_chado/includes/tripal_chado.install.inc
  51. 18 4
      tripal_chado/includes/tripal_chado.semweb.inc
  52. 56 6
      tripal_chado/includes/tripal_chado.vocab_storage.inc
  53. 117 0
      tripal_chado/tripal_chado.install
  54. 32 36
      tripal_ws/api/tripal_ws.api.inc
  55. 79 32
      tripal_ws/includes/TripalFields/remote__data/remote__data.inc
  56. 1 1
      tripal_ws/includes/TripalWebService/TripalContentService_v0_1.inc

+ 1 - 0
docs/dev_guide.rst

@@ -9,6 +9,7 @@ Developer's Guide
    dev_guide/introduction
    dev_guide/data_structures
    dev_guide/best_practices
+   dev_guide/chado
    dev_guide/custom_modules
    dev_guide/custom_field
    dev_guide/custom_data_loader

+ 296 - 0
docs/dev_guide/chado.rst

@@ -0,0 +1,296 @@
+Accessing Chado
+================
+
+Primarily biological data made available to Tripal is stored in the GMOD Chado
+schema. As such, you will likely need to interact with Chado at some point.
+Tripal has developed a number of API functions and classes to make this
+interaction easier and more generic.
+
+The Chado Query API
+--------------------
+
+Provides an API for querying of chado including inserting, updating, deleting and selecting from specific chado tables. There is also a generic function, ``chado_query()``, to execute and SQL statement on chado. It is ideal to use these functions to interact with chado in order to keep your module compatible with both local & external chado databases. Furthermore, it ensures connection to the chado database is taken care of for you.
+
+Generic Queries to a specifc chado table
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Selecting Records
+""""""""""""""""""
+
+``chado_select_record( [table name], [columns to select], [specify record to select], [options*] )``
+
+This function allows you to select various columns from the specified chado table. Although you can only select from a single table, you can specify the record to select using values from related tables through use of a nested array. For example, the following code shows you how to select the name and uniquename of a feature based on it's type and source organism.
+
+.. code-block:: php
+
+  $values =  array(
+    'organism_id' => array(
+      'genus' => 'Citrus',
+      'species' => 'sinensis',
+    ),
+    'type_id' => array (
+      'cv_id' => array (
+        'name' => 'sequence',
+      ),
+      'name' => 'gene',
+      'is_obsolete' => 0
+    ),
+  );
+
+  $result = chado_select_record(
+    'feature',                      // table to select from
+    array('name', 'uniquename'),    // columns to select
+    $values                         // record to select (see variable defn. above)
+  );
+
+Inserting Records
+""""""""""""""""""
+
+``chado_insert_record( [table name], [values to insert], [options*] )``
+
+This function allows you to insert a single record into a specific table. The values to insert are specified using an associative array where the keys are the column names to insert into and they point to the value to be inserted into that column. If the column is a foreign key, the key will point to an array specifying the record in the foreign table and then the primary key of that record will be inserted in the column. For example, the following code will insert a feature and for the type_id, the cvterm.cvterm_id of the cvterm record will be inserted and for the organism_id, the organism.organism_id of the organism_record will be inserted.
+
+.. code-block:: php
+
+  $values =  array(
+    'organism_id' => array(
+        'genus' => 'Citrus',
+        'species' => 'sinensis',
+     ),
+    'name' => 'orange1.1g000034m.g',
+    'uniquename' => 'orange1.1g000034m.g',
+    'type_id' => array (
+        'cv_id' => array (
+           'name' => 'sequence',
+        ),
+        'name' => 'gene',
+        'is_obsolete' => 0
+     ),
+  );
+  $result = chado_insert_record(
+    'feature',             // table to insert into
+    $values                // values to insert
+  );
+
+Updating Records
+""""""""""""""""""
+
+``chado_update_record( [table name], [specify record to update], [values to change], [options*] )``
+
+This function allows you to update records in a specific chado table. The record(s) you wish to update are specified the same as in the select function above and the values to be update are specified the same as the values to be inserted were. For example, the following code species that a feature with a given uniquename, organism_id, and type_id (the unique constraint for the feature table) will be updated with a new name, and the type changed from a gene to an mRNA.
+
+.. code-block:: php
+
+  $umatch = array(
+    'organism_id' => array(
+      'genus' => 'Citrus',
+      'species' => 'sinensis',
+    ),
+    'uniquename' => 'orange1.1g000034m.g7',
+    'type_id' => array (
+      'cv_id' => array (
+        'name' => 'sequence',
+      ),
+      'name' => 'gene',
+      'is_obsolete' => 0
+    ),
+  );
+  $uvalues = array(
+    'name' => 'orange1.1g000034m.g',
+    'type_id' => array (
+      'cv_id' => array (
+        'name' => 'sequence',
+      ),
+      'name' => 'mRNA',
+      'is_obsolete' => 0
+    ),
+  );
+  $result = chado_update_record('feature',$umatch,$uvalues);
+
+Deleting Records
+"""""""""""""""""
+
+``chado_delete_record( [table name], [specify records to delete], [options*] )``
+
+This function allows you to delete records from a specific chado table. The record(s) to delete are specified the same as the record to select/update was above. For example, the following code will delete all genes from the organism Citrus sinensis.
+
+.. code-block:: php
+
+  $values =  array(
+    'organism_id' => array(
+        'genus' => 'Citrus',
+        'species' => 'sinensis',
+     ),
+    'type_id' => array (
+        'cv_id' => array (
+           'name' => 'sequence',
+        ),
+        'name' => 'gene',
+        'is_obsolete' => 0
+     ),
+  );
+  $result = chado_select_record(
+     'feature',                      // table to select from
+     $values                         // records to delete (see variable defn. above)
+  );
+
+Generic Queries for any SQL
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Often it is necessary to select from more then one table in chado or to execute other complex queries that cannot be handled efficiently by the above functions. It is for this reason that the ``chado_query( [sql string], [arguments to sub-in to the sql] )`` function was created. This function allows you to execute any SQL directly on the chado database and should be used with care. If any user input will be used in the query make sure to put a placeholder in your SQL string and then define the value in the arguments array. This will make sure that the user input is sanitized and safe through type-checking and escaping. The following code shows an example of how to use user input resulting from a form and would be called with the form submit function.
+
+.. code-block:: php
+
+  $sql = "SELECT F.name, CVT.name as type_name, ORG.common_name
+           FROM feature F
+           LEFT JOIN cvterm CVT ON F.type_id = CVT.cvterm_id
+           LEFT JOIN organism ORG ON F.organism_id = ORG.organism_id
+           WHERE
+             F.uniquename = :feature_uniquename";
+  $args = array( ':feature_uniquename' => $form_state['values']['uniquename'] );
+  $result = chado_query( $sql, $args );
+  foreach ($result as $r) { [Do something with the records here] }
+
+If you are going to need more then a couple fields, you might want to use the Chado Variables API (specifically ``chado_generate_var()``) to select all of the common fields needed including following foreign keys.
+
+Loading of Variables from chado data
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+These functions, ``chado_generate_var()`` and ``chado_expand_var()``, generate objects containing the full details of a record(s) in chado. These should be used in all theme templates.
+
+This differs from the objects returned by ``chado_select_record`` in so far as all foreign key relationships have been followed meaning you have more complete details. Thus this function should be used whenever you need a full variable and ``chado_select_record`` should be used if you only case about a few columns.
+
+The initial variable is generated by the ``chado_generate_var([table], [filter criteria], [optional options])`` function. An example of how to use this function is:
+
+.. code-block:: php
+
+  $values = array(
+    'name' => 'Medtr4g030710'
+  );
+  $features = chado_generate_var('feature', $values);
+
+This will return an object if there is only one feature with the name Medtr4g030710 or it will return an array of feature objects if more than one feature has that name.
+
+Some tables and fields are excluded by default. To have those tables & fields added to your variable you can use the ``chado_expand_var([chado variable], [type], [what to expand], [optional options])`` function. An example of how to use this function is:
+
+.. code-block:: php
+
+  // Get a chado object to be expanded
+  $values = array(
+    'name' => 'Medtr4g030710'
+  );
+
+  $features = chado_generate_var('feature', $values);
+
+  // Expand the organism node
+  $feature = chado_expand_var($feature, 'node', 'organism');
+
+  // Expand the feature.residues field
+  $feature = chado_expand_var($feature, 'field', 'feature.residues');
+
+  // Expand the feature properties (featureprop table)
+  $feature = chado_expand_var($feature, 'table', 'featureprop');
+
+
+The Chado Schema API
+--------------------
+
+The Chado Schema API provides an application programming interface (API) for describing Chado tables, accessing these descriptions and checking for compliancy of your current database to the chado schema. This API consists of the ChadoSchema class which provides methods for interacting with the Chado Schema API and a collection of supporting functions, one for each table in Chado, which describe each version of the Chado schema. Each function simply returns a Drupal style array that defines the table.
+
+Ensuring columns Tables & Columns exist
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Generally you can assume the tables and columns in the Chado schema have been unaltered. That said, there are still cases where you might want to check that specific tables and columns exist. For example, when using a custom table, it is best practice to ensure it is there before querying as it can be removed through the administrative interface.
+
+To check the existence of a specific table and column, you can use the following:
+
+.. code-block:: php
+
+  $chado_schema = new \ChadoSchema();
+
+  // Check that the organism_feature_count custom table exists.
+  $table_name = 'organism_feature_count';
+  $table_exists = $chado_schema->checkTableExists($table_name);
+
+  if ($table_exists) {
+
+    // Check that the organism_feature_count.feature_id column exists.
+    $column_name = 'feature_id';
+    $column_exists = $chado_schema->checkColumnExists($table_name, $column_name);
+
+    if ($column_exists) {
+
+      [ do your query, etc. here ]
+
+    } else { [warn the admin using tripal_repot_error()] }
+  } else { [warn the admin using tripal_repot_error()] }
+
+Checking the Schema Version
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you are using chado tables specific to a given version of Chado, it is best practice to check the chado version of the current site before querying those tables. You can use the following query to do this:
+
+.. code-block:: php
+
+  $chado_schema = new \ChadoSchema();
+  $version = $chado_schema-getVersion();
+  if ($version == '1.3') {
+    [do your chado v1.3 specific querying here]
+  } else { [warn the admin using tripal_report_error() ] }
+
+
+Retrieving a list of tables
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To retrieve a list of Chado tables, you can use the following:
+
+.. code-block:: php
+
+  $chado_schema = new \ChadoSchema();
+
+  // All Chado Tables including custom tables
+  $all_tables = $chado_schema->getTableNames(TRUE);
+
+  // All Chado Tables without custom tables
+  $all_tables = $chado_schema->getTableNames();
+
+  // Chado tables designated as Base Tables by Tripal.
+  $base_tables = $chado_schema->getBaseTables();
+
+
+Ensuring your Chado instance is compliant
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Checking compliancy of your Chado instance with the released Chado Schema is a great way to **confirm an upgrade has gone flawlessly**. Additionally, while it is not recommended, sometimes customizations to the Chado schema may be necessary. In these cases, you should **ensure backwards compatibility** through compliance checking to confirm Tripal will work as expected.
+
+Chado compliancy testing is provided with Tripal's automated PHPUnit testing. As such, to test compliancy of your specific Chado instance, you first need to install Composer. Luckily this can be as easy as:
+
+.. code-block:: bash
+
+  php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
+  php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
+  php composer-setup.php
+  php -r "unlink('composer-setup.php');"
+
+Once you have Composer, you need to install PHPUnit. This is installed locally within your Tripal repository. The following bash snippet shows you how to both install composer locally and run compliance checking.
+
+.. code-block:: php
+
+  cd [DRUPAL_ROOT]/sites/all/modules/tripal
+  composer up
+
+  # Now run compliance checking
+  ./vendor/bin/phpunit --group chado-compliance
+
+Schema Definition
+^^^^^^^^^^^^^^^^^^
+
+To retrieve the schema definition for a specific table, you can execute the following:
+
+.. code-block:: php
+
+  $table_name = 'feature';
+  $chado_schema = new \ChadoSchema();
+  $table_schema = $chado_schema->getTableSchema($table_name);
+
+The resulting ``$table_schema`` variable contains a Drupal-style array describing the schema definition of the table specified by ``$table_name``. This is a great tool when trying to develop generic queries, since you can extract information about an unknown table and use it to build a query for that table. For more information on the format of this array, see `the Drupal Schema API documentation <https://api.drupal.org/api/drupal/includes%21database%21schema.inc/group/schemaapi/7.x>`_.

+ 1 - 1
docs/dev_guide/tutorials.rst

@@ -15,7 +15,7 @@ The `Tripal Test Suite module <https://github.com/statonlab/TripalTestSuite>`_ i
 
 * `Creating and running Test Suite Tests <https://www.youtube.com/watch?v=hxuiDzRqs9U>`_
 * `Test Suite Factories and DB Transactions <https://www.youtube.com/watch?v=PTJ1Dv8QAag>`_
-
+* `Test Suite Seeding and Publishing Data <https://www.youtube.com/watch?v=HE2B7YnWYfQ>`_
 * `Test Driven Development live demo <https://www.youtube.com/watch?v=zmYZ_HV3b9s>`_
 
 Developer Tools

+ 1 - 1
docs/user_guide.rst

@@ -1,5 +1,5 @@
 User's Guide
-==============
+============
 
 
 .. toctree::

+ 11 - 4
docs/user_guide/bulk_loader.rst

@@ -1,7 +1,10 @@
-
 Bulk Loader
 ===========
 
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`./install_tripal/drupal_home`
+
 The bulk loader is a tool that Tripal provides for loading of data contained in tab delimited files. Tripal supports loading of files in standard formats (e.g. ``FASTA``, ``GFF``, ``OBO``), but Chado can support a variety of different biological data types and there are often no community standard file formats for loading these data. For example, there is no file format for importing genotype and phenotype data. Those data can be stored in the feature, stock and natural diversity tables of Chado. The Bulk Loader was introduced in Tripal v1.1 and provides a web interface for building custom data loader. In short, the site developer creates the bulk loader "template". This template can then be used and re-used for any tab delimited file that follows the format described by the template. Additionally, bulk loading templates can be exported allowing Tripal sites to share loaders with one another.  Loading templates that have been shared are available on the Tripal website here: http://tripal.info/extensions/bulk-loader-templates.
 
 The following commands can be executed to install the Tripal Bulk Loader using Drush:
@@ -20,7 +23,7 @@ To demonstrate use of the Bulk Loader, a brief example that imports a list of or
 
 .. code-block bash
 
-  cd /var/www/html/sites/default/files
+  cd $DRUPAL_HOME/sites/default/files
   wget http://tripal.info/sites/default/files/book_pages/Fragaria_0.txt
 
 
@@ -244,10 +247,14 @@ Provide the following values:
 
 * Job Name: Import of Fragaria species
 * Template: NCBI Taxonomy Importer (taxid, genus species).
-* Data File: /var/www/html/sites/default/files/Fragaria_0.txt
+* Data File: [DRUPAL_HOME]/sites/default/files/Fragaria_0.txt
 * Keep track of inserted IDs: No
 * File has a header: No
 
+.. note::
+
+  Be sure to change the [DRUPAL_HOME] token to where Drupal is installed.
+
 Click **Save**. The page then appears as follows:
 
 .. image:: ./bulk_loader.9.png
@@ -261,7 +268,7 @@ Now that we have created a job, we can submit it for execution by clicking the *
 .. code-block:: shell
 
   cd /var/www
-  drush trp-run-jobs --username=admin --root=/var/www/html
+  drush trp-run-jobs --username=admin --root=$DRUPAL_HOME
 
 After execution of the job you should see similar output to the terminal window:
 

+ 8 - 3
docs/user_guide/example_genomics/pub_import.rst

@@ -1,5 +1,9 @@
 Importing Publications
 ======================
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`../install_tripal/drupal_home`
+  
 Tripal provides an interface for automatically and manually adding publications.
 
 Manually Adding a Publication
@@ -81,7 +85,7 @@ Next, there are two ways to import these publications. The first it to manually
 
 ::
 
-  cd /var/www/html
+  cd $DRUPAL_HOME
   drush trp-import-pubs --username=administrator
 
 You should see output to the terminal that begins like this:
@@ -105,7 +109,7 @@ Some things to know about the publication importer:
 
   ::
 
-    cd /var/www/html
+    cd $DRUPAL_HOME
     drush trp-run-jobs --user=administrator
 
 The second way to import publications is to add an entry to the UNIX cron. We did this previously for the Tripal Jobs management system when we first installed Tripal. We will add another entry for importing publications. But first, now that we have imported all of the relevant pubs, we need to return to the importers list at **Tripal → Data Loaders → Chado Publication Importers** and disable the first importer we created. We do not want to run that importer again, as we've already imported all historical publications on record at PubMed. Click the edit button next to the importer named Pubs for Citrus sinensis, click the disable checkbox and then save the template. The template should now be disabled.
@@ -120,11 +124,12 @@ Now add the following line to the bottom of the crontab:
 
 ::
 
-  30 8 1,15 * *  su - www-data -c '/usr/local/drush/drush -r /var/www/html -l http://[site url] trp-import-pubs --report=[your email] > /dev/null'
+  30 8 1,15 * *  su - www-data -c '/usr/local/drush/drush -r [DRUPAL_HOME] -l http://[site url] trp-import-pubs --report=[your email] > /dev/null'
 
 Where
 
 - [site url] is the full URL of your site
 - [your email] is the email address of the user that should receive an email containing a list of publications that were imported. You can separate multiple email addresses with a comma.
+- [DRUPAL_HOME] is the directory where Drupal is installed
 
 The cron entry above will launch the importer at 8:30am on the first and fifteenth days of the month. We will run this importer twice a month in the event it fails to run (e.g. server is down) at least one time during the month.

+ 1 - 0
docs/user_guide/install_tripal.rst

@@ -8,6 +8,7 @@ Install Tripal
    :glob:
 
    ./install_tripal/pre_planning
+   ./install_tripal/drupal_home
    ./install_tripal/server_setup
    ./install_tripal/drush_installation
    ./install_tripal/rapid_install

+ 7 - 3
docs/user_guide/install_tripal/automating_job_execution.rst

@@ -1,6 +1,10 @@
 Automating Job Execution
 ========================================
 
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`./drupal_home`
+
 The Drupal cron is used to automatically execute necessary Drupal housekeeping tasks on a regular interval.  You should *always* setup the Drupal cron to ensure your site checks for updates and security issues.  To do this, we want to integrate Drupal cron with the UNIX cron facility.  The UNIX cron will automatically execute commands on set regular intervals.  First, we must get the appropriate URL for the cron by navigating to **Configuration → Cron**. On this page you will see a link that we will use for cron:
 
 .. image:: automating_job_execution.cron.png
@@ -36,9 +40,9 @@ Any job that is added to the Job's system can be run manually on the command lin
 
 .. code-block:: bash
 
-  drush trp-run-jobs --username=administrator --root=/var/www/html
+  drush trp-run-jobs --username=administrator --root=$DRUPAL_HOME
 
-Remember to change the username from **administrator** to the name of the administrator on your site and change **/var/www/html** to the location where your site installed on the server.
+Remember to change the username from **administrator** to the name of the administrator on your site.
 
 Option #2: Additional Cron Entry
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -53,7 +57,7 @@ Add this line to the crontab:
 
 .. code-block:: bash
 
-  0,5,10,15,20,25,30,35,40,45,50,55 * * * * drush trp-run-jobs --username=administrator --root=/var/www/html
+  0,5,10,15,20,25,30,35,40,45,50,55 * * * * drush trp-run-jobs --username=administrator --root=$DRUPAL_HOME
 
 Here, job execution will occur every 5 minutes.
 

+ 13 - 0
docs/user_guide/install_tripal/drupal_home.rst

@@ -0,0 +1,13 @@
+DRUPAL_HOME Variable
+====================
+An important convention in this document is the use of the ``$DRUPAL_HOME`` environment variable.  If you are new to UNIX/Linux you can learn about environment variables `here <https://www.tutorialspoint.com/unix/unix-environment.htm>`_.  Drupal is a necessary depenency of Tripal.  The setup and installation sections describe how to install Drupal.  If you follow the instructions exactly as described in this User's Guide you will install Drupal into ``/var/www/html``. However, some may desire to install Drupal elsewhere.  To ensure that all command-line examples in this guide can be cut-and-pasted you **must** set the ``$DRUPAL_HOME`` variable.  You can set the variable in the following way:
+
+  .. code-block:: bash
+
+    DRUPAL_HOME=/var/www/html
+    
+Be sure to change the path ``/var/www/html`` to the location where you have installed Drupal.  If you have never installed Drupal and you intend on following this guide step-by-step then use the command-line above to get started.
+
+.. note::
+
+  You will have to set the ``$DRUPAL_HOME`` environment variable anytime you open a new terminal window.

+ 25 - 20
docs/user_guide/install_tripal/manual_install/install_drupal.rst

@@ -33,29 +33,32 @@ We no longer need to be the postgres user so exit
 Software Installation
 ---------------------
 
-We want to install Drupal into our web document root (/var/www/html).   Before we can install Drupal we must ensure that that we are allowed to add files into the /var/www/html directory.  Select a user account that will be the owner of all web files and change the owner of the /var/www/html directory to that user:
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`../drupal_home`
+
+
+Before we can install Drupal we must ensure that that we are allowed to add files into the root directory.  Select a user account that will be the owner of all web files and change the owner of the ``$DRUPAL_HOME`` directory to that user:
 
 .. code-block:: bash
 
-  sudo chown -R [user] /var/www/html
+
+  sudo chown -R [user] $DRUPAL_HOME
 
 Substitute [user] for the name of the user that will own the web files.
 
 
 .. note::
 
-  The apache web server runs as the user 'www-data'.  For security reasons you should chose a user other than 'www-data' to be the owner of the /var/www/html directory.
+  The apache web server runs as the user 'www-data'.  For security reasons you should chose a user other than 'www-data' to be the owner of the Drupal root directory.
 
-Tripal 3.x requires version 7.x of Drupal. Drupal can be freely downloaded from the http://www.drupal.org website. At the writing of this Tutorial the most recent version of Drupal 7 is version 7.59. The software can be downloaded manually from the Drupal website through a web browser or we can use the 'wget' command to retrieve it:
+Tripal 3.x requires version 7.x of Drupal. Drupal can be freely downloaded from the http://www.drupal.org website. At the writing of this Tutorial the most recent version of Drupal 7 is version 7.59. The software can be downloaded manually from the Drupal website through a web browser or we can use the ``wget`` command to retrieve it:
 
 .. code-block:: bash
 
-  cd /var/www/html
+  cd $DRUPAL_HOME
   wget http://ftp.drupal.org/files/projects/drupal-7.59.tar.gz
 
-.. note::
-
-  If you are using older version of Ubuntu the web root directory may be /var/www rather than /var/www/html and you may need to change the path accordingly.
 
 Next, we want to install Drupal. We will use the tar command to uncompress the software:
 
@@ -78,11 +81,11 @@ If an index.html file is present (as is the case with Ubuntu installations) you
 
 .. note::
 
-  It is extremely important the the hidden file .htaccess is also moved (note the second 'mv' command above. Check to make sure this file is there
+  It is extremely important the the hidden file ``.htaccess`` is also moved (note the second ``mv`` command above. Check to make sure this file is there:
 
-.. code-block:: bash
+  .. code-block:: bash
 
-  ls -l .htaccess
+    ls -l .htaccess
 
 Configuration File
 ------------------
@@ -93,15 +96,15 @@ First navigate to the location where the configuration file should go:
 
 .. code-block:: bash
 
-  cd /var/www/html/sites/default/
+  cd $DRUPAL_HOME/sites/default/
 
-Next, copy the example configuration that already exists in the directory to be our actual configuration file by renaming it to settings.php.
+Next, copy the example configuration that already exists in the directory to be our actual configuration file by renaming it to ``settings.php``.
 
 .. code-block:: bash
 
   cp default.settings.php settings.php
 
-Now, we need to edit the configuration file to tell Drupal how to connect to our database server. To do this we'll use an easy to use text editor gedit
+Now, we need to edit the configuration file to tell Drupal how to connect to our database server. To do this we'll use an easy to use text editor **gedit**.
 
 .. code-block:: bash
 
@@ -127,17 +130,19 @@ and then insert the following array just after the above line:
   );
 
 Replace the text '********' with your database password for the user 'drupal' created previously.  Save the configuration file and close the editor.
-Files directory creation
 
-Finally, we need to create the directory where Drupal will have write-access to add files.  By default, Drupal expects to have write permission in the /var/www/html/sites/default/files directory.  Therefore, we will set group ownership of the directory to the group used by the Apache web server.  This will be the user that Drupal uses to write files.
+Files Directory Creation
+--------------------------
+
+Finally, we need to create the directory where Drupal will have write-access to add files.  By default, Drupal expects to have write permission in the ``$DRUPAL_HOME/sites/default/files`` directory.  Therefore, we will set group ownership of the directory to the group used by the Apache web server.  This will be the user that Drupal uses to write files.
 
 .. code-block:: bash
 
-  mkdir -p /var/www/html/sites/default/files
-  sudo chgrp [group] /var/www/html/sites/default/files
-  sudo chmod g+rw /var/www/html/sites/default/files
+  mkdir -p $DRUPAL_HOME/sites/default/files
+  sudo chgrp [group] $DRUPAL_HOME/sites/default/files
+  sudo chmod g+rw $DRUPAL_HOME/sites/default/files
 
-Substitute [group] for the name of the web server's group.  In Ubuntu this is www-data in CentOS this is apache.The above commands creates the directory, sets the group ownership for group. and gives read/write permissions to the group on the directory.
+Substitute [group] for the name of the web server's group.  In Ubuntu this is www-data in CentOS this is apache. The above commands creates the directory, sets the group ownership for group, and gives read/write permissions to the group on the directory.
 
 Web-based Steps
 ---------------

+ 8 - 3
docs/user_guide/install_tripal/manual_install/install_prereqs.rst

@@ -1,13 +1,18 @@
 Tripal Prerequisites
 ====================
 
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`../drupal_home`
+
+
 Tripal v3.x requires several Drupal modules. These include  `Entity <https://www.drupal.org/project/entity>`_,  `Views <https://www.drupal.org/project/views>`_, `CTools <https://www.drupal.org/project/ctools>`_, `Display Suite <https://www.drupal.org/project/ds>`_, `Field Group <https://www.drupal.org/project/field_group>`_, `Field Group Table <https://www.drupal.org/project/field_group_table>`_, `Field Formatter Class <https://www.drupal.org/project/field_formatter_class>`_ and `Field Formatter Settings <https://www.drupal.org/project/field_formatter_settings>`_ modules.   Modules can be installed using the graphical Drupal website by clicking on the Modules link in the top adminstrative menu bar.  Instructions for instaling Modules via the web-interface can be found here:  https://www.drupal.org/documentation/install/modules-themes/modules-7. However, Drush can be quicker for module installation. The following instructions will show how to install a module using the Drush command-line tool.
 
 First, install the Entity module.  We will download the current version using the drush command. On the command-line, execute the following:
 
 .. code-block:: bash
 
-  cd /var/www/html/sites/all/modules
+  cd $DRUPAL_HOME/sites/all/modules
   drush pm-download entity
 
 Typically for all module installation we should check the README for additional installation instructions. Next, enable the module using a drush command:
@@ -23,14 +28,14 @@ For basic Tripal functionality you must also enable the Views and CTools modules
   drush pm-download views ctools
   drush pm-enable views views_ui ctools
 
-Finally, Tripal works best when it can provide default display layouts.   To support default layouts you must also enable the remainig dependencies:
+Finally, Tripal works best when it can provide default display layouts.   To support default layouts you must also enable the remaining dependencies:
 
 .. code-block:: bash
 
   drush pm-download ds field_group field_group_table field_formatter_class field_formatter_settings
   drush pm-enable ds field_group field_group_table field_formatter_class field_formatter_settings
 
-Optionally, you can install the ckeditor module.  This module provides a nice WYSIWYG editor that allows you to edit text on your site using a graphical editor. Otherwise, if you need images or formatting (e.g. underline, bold, headers) you would be required to write HTML.   It is recommended that this module be installed to improve the user experience:
+Optionally, you can install the ckeditor module.  This module provides a nice WYSIWYG editor that allows you to edit text on your site using a graphical editor. Otherwise, if you need images or formatting (e.g. underline, bold, headers) you would be required to write HTML.  It is recommended that this module be installed to improve the user experience:
 
 .. code-block:: bash
 

+ 11 - 6
docs/user_guide/install_tripal/manual_install/install_tripal.rst

@@ -1,6 +1,11 @@
 Tripal Installation
 ===================
 
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`../drupal_home`
+
+
 Before installation of Tripal, you must first have a working Drupal installation.  Please see the previous section of this tutorial for step-by-step examples for server setup and Drupal installation instructions.  After installation of Tripal, you may install any Tripal extension modules you may want.
 
 Download Tripal
@@ -24,15 +29,15 @@ A bug exists in Drupal related to the bytea data type in PostgreSQL. At the writ
 
 .. code-block:: bash
 
-  cd /var/www/html
+  cd $DRUPAL_HOME
   wget --no-check-certificate https://drupal.org/files/drupal.pgsql-bytea.27.patch
   patch -p1 < drupal.pgsql-bytea.27.patch
 
-There is also a bug in the Drupal Views 3.0 code that prevents Tripal's administrative and search data views from functioning. The patch is provided within the tripal_veiws module. To apply the patch execute the following:
+There is also a bug in the Drupal Views 3.0 code that prevents Tripal's administrative and search data views from functioning. The patch is provided within the tripal_views module. To apply the patch execute the following:
 
 .. code-block:: bash
 
-  cd /var/www/html/sites/all/modules/views
+  cd $DRUPAL_HOME/sites/all/modules/views
   patch -p1 < ../tripal/tripal_chado_views/views-sql-compliant-three-tier-naming-1971160-30.patch
 
 Install Tripal
@@ -74,7 +79,7 @@ Jobs in the queue can be executed using drush to manually launch the job:
 
 .. code-block:: bash
 
-  drush trp-run-jobs --username=administrator --root=/var/www/html
+  drush trp-run-jobs --username=administrator --root=$DRUPAL_HOME
 
 As the installation of Chado proceeds, we should see output on the terminal console indicating the progress of the installation.  You should see output similar to the following:
 
@@ -108,7 +113,7 @@ To prepare the site click the button Prepare this site.   A new job is added to
 
 .. code-block:: bash
 
-  drush trp-run-jobs --username=administrator --root=/var/www/html
+  drush trp-run-jobs --username=administrator --root=$DRUPAL_HOME
 
 .. note::
 
@@ -125,4 +130,4 @@ Preparing Chado and Drupal in a previous step resulted in the automatic creation
 
 .. image:: install_tripal.install7.png
 
-Review these permissions and set them according to how you want content to be managed.   Typically, the administrator user receives all permissions, and anonymous and authenticated users receive 'View' permissions for all content types.  If you desire to create other types of users, Drupal allows you to do this by creating new types of roles.  For example, if you know that some users will be responsible for curating content, then you may add a curator role by clicking the Roles link in the top right corner of this permissions page.  After the new role is created you can return to the permission page to set the permissions accordingly.
+Review these permissions and set them according to how you want content to be managed.  Typically, the administrator user receives all permissions, and anonymous and authenticated users receive 'View' permissions for all content types.  If you desire to create other types of users, Drupal allows you to do this by creating new types of roles.  For example, if you know that some users will be responsible for curating content, then you may add a curator role by clicking the **Roles** link in the top right corner of this permissions page.  After the new role is created you can return to the permission page to set the permissions accordingly.

+ 13 - 6
docs/user_guide/install_tripal/rapid_install.rst

@@ -1,8 +1,15 @@
 Installation Method #1: Rapid Installation
 ==========================================
 
-Before installing via the rapid installation process please ensure drush is installed, and the server is setup.    Rapid Installation works with Tripal v3.0-rc2 (release candidate 2) and later.   If you are using a previous version of Tripal, please proceed to the step-by-step instructions.
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`./drupal_home`
+
+
+Before installing via the rapid installation process please ensure drush is installed, and the server is setup.   Rapid Installation works with Tripal v3.0-rc2 (release candidate 2) and later.  If you are using a previous version of Tripal, please proceed to the step-by-step instructions.
+
 Database Setup
+---------------
 
 Before we can install Tripal we must have a database ready for it.  In the server setup instructions were provided to set up a PostgreSQL database server. Now, we need to create the Drupal database. To do so we must first become the PostgreSQL user.
 
@@ -10,7 +17,7 @@ Before we can install Tripal we must have a database ready for it.  In the serve
 
   sudo su - postgres
 
-Next, create the new 'drupal' user account. This account will not be a "superuser' nor allowed to create new roles, but should be allowed to create a database.
+Next, create the new 'drupal' user account. This account will not be a "superuser" nor allowed to create new roles, but should be allowed to create a database.
 
 .. code-block:: bash
 
@@ -31,17 +38,17 @@ We no longer need to be the postgres user so exit
 Tripal Installation
 -------------------
 
-Navigate to the directory you want to create the website. For this example it will be /var/www/html (the typical home location for Ubuntu and CentOS)
+Navigate to your Drupal install directory.
 
 .. code-block:: bash
 
-  cd /var/www/html
+  cd $DRUPAL_HOME
 
 .. note::
 
   Make sure you have write permissions within this directory.
 
-Clone the tripal_install project using the git command and move the contents up one level into the web document directory:
+Clone the tripal_install project using the ``git`` command and move the contents up one level into the web document directory:
 
 .. code-block:: bash
 
@@ -70,7 +77,7 @@ From this point onward, you will be asked a series of questions in the terminal
      Administrator password: P@55w0rd
   Is this information correct? (y/n): y
 
-Next, you will be asked for the database information: database name, database  username, database  user password, host, and port.  The database name and user should match what you created in the previous section (i.e. database name = 'drupal' and database user = 'drupal').  The 'host' is the name of the server or its IP address, and the port is a numerical value that PostgreSQL uses for communication.  By default PostgreSQL uses the port 5432.  If a mistake is made you can make corrections as shown in the following screenshot:
+Next, you will be asked for the database information: database name, database  username, database  user password, host, and port.  The database name and user should match what you created in the previous section (i.e. database name = 'drupal' and database user = 'drupal').  The 'host' is the name of the server or its IP address, and the port is a numerical value that PostgreSQL uses for communication.  By default PostgreSQL uses the port 5432.  If a mistake is made you can make corrections as shown below:
 
 ::
 

+ 3 - 3
docs/user_guide/install_tripal/upgrade_from_tripal2.rst

@@ -44,7 +44,7 @@ Step 1: Upgrade Tripal
 
     drush pm-disable tripal_core
 
-4.  The Tripal modules must also be downloaded and updated. To do this, delete the old Tripal v2 modules directories, located in ``sites/all/modules`` from your Drupal root:  for example ``/var/www/html/sites/all/modules``(be sure you have a backup before removing). The following command will retrieve the Tripal 3 version:
+4.  The Tripal modules must also be downloaded and updated. To do this, delete the old Tripal v2 modules directories, located in ``sites/all/modules`` from your Drupal root:  for example ``/var/www/html/sites/all/modules`` (be sure you have a backup before removing). The following command will retrieve the Tripal 3 version:
 
   .. code-block:: bash
 
@@ -137,11 +137,11 @@ The process allows you to create Tripal 3 content types exposing the same data a
 
 4. Select the checkbox beside each Tripal v3 type you would like to create. The number of entities/pages that will be created for that content type is shown in brackets beside the name.
 
-5. Then click the "Migrate [Tripal v2 Type]" button. This will submit a Tripal job to create the requested content. Submit this job manually on the command-line as follows:
+5. Then click the "Migrate [Tripal v2 Type]" button. This will submit a Tripal job to create the requested content. Submit this job manually on the command-line as follows (note we ``cd`` to the project root at ``/var/www/html``: please navigate to wherever your site is installed):
 
   .. code-block:: bash
 
-    cd /var/www/html
+    cd $DRUPAL_HOME
     drush trp-run-jobs --user=administrator
 
 6. Now repeat 1-5 for each content type. Since this step simply creates new Tripal v3 content without touching the existing Tripal v2 content, there really is no reason not to migrate all your content types. Especially since the Tripal v3 content remains private and thus hidden from your users.

+ 6 - 1
docs/user_guide/job_management.rst

@@ -1,6 +1,11 @@
 Job Management (Tripal Daemon)
 ==============================
 
+.. note::
+
+  Remember you must set the $DRUPAL_HOME environment variable to cut-and-paste the commands below. See see :doc:`./install_tripal/drupal_home`
+
+
 The Tripal Daemon module is meant to provide a simple means of creating a robust command-line-driven, fully bootstrapped PHP Daemon. It uses the PHP-Daemon (https://github.com/shaneharter/PHP-Daemon) Library to create the Daemon (via the Libraries API) in order to not re-invent the wheel. It allows you to execute Jobs submitted to Tripal without using cron.  It provides a faster user experience for running jobs.  Prior to Tripal v3, the Tripal Daemon module was an extension module. It was integrated into the core Tripal pacakge.
 
 Features
@@ -27,7 +32,7 @@ Next, we need the `PHP-Daemon Library version 2.0 <https://github.com/shaneharte
 
 .. code-block:: shell
 
-  cd /var/www/html/sites/all/libraries
+  cd $DRUPAL_HOME/sites/all/libraries
   wget https://github.com/shaneharter/PHP-Daemon/archive/v2.0.tar.gz
   tar -zxvf v2.0.tar.gz
   mv v2.0.tar.gz PHP-Daemon

+ 5 - 1
docs/user_guide/mviews.rst

@@ -1,6 +1,10 @@
 Materialized Views
 ==================
 
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`./install_tripal/drupal_home`
+
 Chado is efficient as a data warehouse but queries can become slow depending on the type of query. To help simplify and speed up these queries, materialized views can be employed. For a materialized view, a new database table is created and then populated with the results of a pre-defined SQL query. This allows you to execute a much simpler and faster query on the materialized view when producing user pages. A side effect, however is redundant data, with the materialized view becoming stale if not updated regularly.
 
 Tripal provides a mechanism for populating and updating these materialized views. These can be found on the **Tripal → Data Storage → Chado -> Materialized Views** page.
@@ -13,7 +17,7 @@ This will submit jobs to populate the views with data. Now, run the jobs:
 
 .. code-block:: shell
 
-  cd /var/www/html
+  cd $DRUPAL_HOME
   drush trp-run-jobs --user=administrator
 
 You can now see that all views are up-to-date on the **Materialized Views Page**. The number of rows in the view table is shown.

+ 5 - 1
docs/user_guide/web_services.rst

@@ -1,6 +1,10 @@
 Web Services
 ============
 
+.. note::
+
+  Remember you must set the ``$DRUPAL_HOME`` environment variable if you want to cut-and-paste the commands below. See :doc:`./install_tripal/drupal_home`
+
 Overview
 --------
 
@@ -31,7 +35,7 @@ To enable web services, simply install the ``tripal_ws`` module, either using th
 
 .. code-block:: shell
 
-  cd /var/www/html
+  cd $DRUPAL_HOME
   drush pm-enable tripal_ws
 
 Exploring Web Services

+ 137 - 0
tests/tripal_chado/api/ChadoComplianceTest.php

@@ -0,0 +1,137 @@
+<?php
+namespace Tests\tripal_chado\api;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+
+module_load_include('inc', 'tripal_chado', 'api/ChadoSchema');
+
+/**
+ * Tests the current Chado Database is compliant with the schema definition used by Tripal
+ */
+class ChadoComplianceTest extends TripalTestCase {
+  // Uncomment to auto start and rollback db transactions per test method.
+  use DBTransaction;
+
+  /**
+   * DataProvider, a list of all chado tables.
+   *
+   * @return array
+   */
+  public function chadoTableProvider() {
+
+    $chado_schema = new \ChadoSchema();
+    $version = $chado_schema->getVersion();
+
+    $dataset = [];
+    foreach ($chado_schema->getTableNames() as $table_name) {
+      $dataset[] = [$version, $table_name];
+    }
+
+    return $dataset;
+  }
+
+  /**
+   * Tests Compliance for a given table.
+   *
+   * The following is tested:
+   *   1. The table exists in the correct schema.
+   *   2. It has all the fields we expect.
+   *   3. Each field is the type we expect.
+   *   4. It has all the constraints we expect.
+   *   5. Each constraint consists of the columns we expect.
+   *
+   * @dataProvider chadoTableProvider
+   *
+   * @group api
+   * @group chado
+   * @group chado-compliance
+   */
+  public function testTableCompliance($schema_version, $table_name) {
+
+    // Create the ChadoSchema class to aid in testing.
+    $chado_schema = new \ChadoSchema();
+    $version = $chado_schema->getVersion();
+    $schema_name = $chado_schema->getSchemaName();
+
+    // Check #1: The table exists in the correct schema.
+    $this->assertTrue(
+      $chado_schema->checkTableExists($table_name),
+      t('"!table_name" should exist in the "!chado" schema v!version.',
+        array('!table_name' => $table_name, '!chado' => $schema_name, '!version' => $version))
+    );
+
+    // Retrieve the schema for this table.
+    $table_schema = $chado_schema->getTableSchema($table_name);
+
+    // For each column in this table...
+    foreach ($table_schema['fields'] as $column_name => $column_details) {
+
+      // Check #2: The given field exists in the table.
+      $this->assertTrue(
+        $chado_schema->checkColumnExists($table_name, $column_name),
+        t('The column "!column" must exist in "!table" for chado v!version.',
+          array('!column' => $column_name, '!table' => $table_name, '!version' => $version))
+      );
+
+      // Check #3: The field is the type we expect.
+      $this->assertTrue(
+        $chado_schema->checkColumnType($table_name, $column_name, $column_details['type']),
+        t('The column "!table.!column" must be of type "!type" for chado v!version.',
+          array('!column' => $column_name, '!table' => $table_name,
+            '!version' => $version, '!type' => $column_details['type']))
+      );
+    }
+
+    // There are three types of constraints:
+    // primary key, unique keys, and foreign keys.
+    //.......................................
+
+    // For the primary key:
+    // Check #4: The constraint exists.
+    if (isset($table_schema['primary key'][0]) AND !empty($table_schema['primary key'][0])) {
+      $pkey_column = $table_schema['primary key'][0];
+      $this->assertTrue(
+        $chado_schema->checkPrimaryKey($table_name, $pkey_column),
+        t('The column "!table.!column" must be a primary key with an associated sequence and constraint for chado v!version.',
+          array('!column' => $pkey_column, '!table' => $table_name, '!version' => $version))
+      );
+    }
+
+    // For each unique key:
+    foreach ($table_schema['unique keys'] as $constraint_name => $columns) {
+      // @debug print "Check '$constraint_name' for '$table_name': ".implode(', ', $columns).".\n";
+
+      // Check #4: The constraint exists.
+      $this->assertTrue(
+        $chado_schema->checkConstraintExists($table_name, $constraint_name, 'UNIQUE'),
+        t('The unique constraint "!name" for "!table" must exist for chado v!version.',
+          array('!name' => $constraint_name, '!table' => $table_name, '!version' => $version))
+      );
+
+      // Check #5: The constraint consists of the columns we expect.
+      // @todo
+    }
+
+    // For each foreign key:
+    foreach ($table_schema['foreign keys'] as $fk_table => $details) {
+      foreach ($details['columns'] as $base_column => $fk_column) {
+        // @debug print "Check '$table_name.$base_column =>  $fk_table.$fk_column ' foreign key.";
+
+        // Check #4: The constraint exists.
+        $constraint_name = $table_name . '_' . $base_column . '_fkey';
+        $this->assertTrue(
+          $chado_schema->checkFKConstraintExists($table_name, $base_column),
+          t('The foreign key constraint "!name" for "!table.!column" => "!fktable.!fkcolumn" must exist for chado v!version.',
+            array('!name' => $constraint_name,
+              '!table' => $table_name, '!column' => $base_column,
+              '!fktable' => $fk_table, '!fkcolumn' => $fk_column,
+              '!version' => $version))
+        );
+
+        // Check #5: The constraint consists of the columns we expect.
+        // @todo
+      }
+    }
+  }
+}

+ 313 - 0
tests/tripal_chado/api/ChadoSchemaTest.php

@@ -0,0 +1,313 @@
+<?php
+namespace Tests\tripal_chado\api;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+use Faker\Factory;
+
+module_load_include('inc', 'tripal_chado', 'api/ChadoSchema');
+
+/**
+ * Tests the ChadoSchema class.
+ *
+ * @todo test "Check" functions in the ChadoSchema class.
+ */
+class ChadoSchemaTest extends TripalTestCase {
+  use DBTransaction;
+
+  /**
+   * Tests that the class can be initiated with or without a record specified
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testInitClass() {
+
+    // Test with no parameters.
+    $chado_schema = new \ChadoSchema();
+    $this->assertNotNull($chado_schema);
+
+    // Test with version.
+    $chado_schema = new \ChadoSchema('1.3');
+    $this->assertNotNull($chado_schema);
+  }
+
+  /**
+   * Tests the ChadoSchema->getVersion() method.
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testGetVersion() {
+
+    // Generate a fake version.
+    $faker = Factory::create();
+    $version = $faker->randomFloat(2, 1, 5);
+
+    // Check version can be retrieved when we set it.
+    $chado_schema = new \ChadoSchema($version);
+    $retrieved_version = $chado_schema->getVersion();
+    $this->assertEquals(
+      $version,
+      $retrieved_version,
+      t('The version retrieved via ChadoSchema->getVersion, "!ret", should equal that set, "!set"',
+        array('!ret' => $retrieved_version, '!set' => $version))
+    );
+
+    // @todo Check version can be retrieved when it's looked up?
+  }
+
+  /**
+   * Tests the ChadoSchema->getSchemaName() method.
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testGetSchemaName() {
+
+    // Generate a fake version.
+    $faker = Factory::create();
+    $version = $faker->randomFloat(2, 1, 5);
+    $schema_name = $faker->word();
+
+    // Check the schema name can be retrieved when we set it.
+    $chado_schema = new \ChadoSchema($version, $schema_name);
+    $retrieved_schema = $chado_schema->getSchemaName();
+    $this->assertEquals(
+      $schema_name,
+      $retrieved_schema,
+      t('The schema name retrieved via ChadoSchema->getSchemaName, "!ret", should equal that set, "!set"',
+        array('!ret' => $retrieved_schema, '!set' => $schema_name))
+    );
+
+    // @todo Check schema name can be retrieved when it's looked up?
+  }
+
+  /**
+   * Tests ChadoSchema->getTableNames() method.
+   *
+   * @dataProvider knownTableProvider
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testGetTableNames($version, $known_tables) {
+
+    // Check: Known tables for a given version are returned.
+    $chado_schema = new \ChadoSchema($version);
+    $returned_tables = $chado_schema->getTableNames();
+
+    foreach ($known_tables as $table_name) {
+      $this->assertArrayHasKey(
+        $table_name,
+        $returned_tables,
+        t('The table, "!known", should exist in the returned tables list for version !version.',
+          array(':known' => $table_name, ':version' => $version))
+      );
+    }
+  }
+
+  /**
+   * Tests ChadoSchema->getTableSchema() method.
+   *
+   * @dataProvider chadoTableProvider
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testGetTableSchema($version, $table_name) {
+
+    // Check: a schema is returned that matches what we expect.
+    $chado_schema = new \ChadoSchema($version);
+
+    $table_schema = $chado_schema->getTableSchema($table_name);
+
+    $this->assertNotEmpty(
+      $table_schema,
+      t('Returned schema for "!table" in chado v!version should not be empty.',
+        array('!table' => $table_name, '!version' => $version))
+    );
+
+    $this->assertArrayHasKey(
+      'fields',
+      $table_schema,
+      t('The schema array for "!table" should have columns listed in an "fields" array',
+        array('!table' => $table_name))
+    );
+
+    // Instead of asserting these keys exist. Lets assert that if they do exist,
+    // they match the expected format.
+
+    if (isset($table_schema['primary key'])) {
+      $this->assertTrue(is_array($table_schema['primary key']),
+	t('The primary key of the Tripal Schema definition for "!table" must be an array.',
+          array('!table' => $table_name)));
+  
+    }
+
+    $this->assertArrayHasKey(
+      'foreign keys',
+      $table_schema,
+      t('The schema array for "!table" should have foreign keys listed in an "foreign keys" array',
+        array('!table' => $table_name))
+    );
+
+  }
+
+  /**
+   * Tests ChadoSchema->getCustomTableSchema() method.
+   *
+   * @dataProvider knownCustomTableProvider
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testGetCustomTableSchema($table_name) {
+
+    // Check: a schema is returned that matches what we expect.
+    $chado_schema = new \ChadoSchema();
+    $table_schema = $chado_schema->getCustomTableSchema($table_name);
+
+    $this->assertNotEmpty(
+      $table_schema,
+      t('Returned schema for "!table" in chado v!version should not be empty.',
+        array('!table' => $table_name, '!version' => $version))
+    );
+
+    $this->assertArrayHasKey(
+      'fields',
+      $table_schema,
+      t('The schema array for "!table" should have columns listed in an "fields" array',
+        array('!table' => $table_name))
+    );
+
+    // NOTE: Other then ensuring fields are set, we can't test further since all other
+    // keys are technically optional and these arrays are set by admins.
+
+  }
+
+  /**
+   * Tests ChadoSchema->getBaseTables() method.
+   *
+   * @dataProvider knownBaseTableProvider
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+  public function testGetBaseTables($version, $known_tables) {
+
+    // Check: Known base tables for a given version are returned.
+    $chado_schema = new \ChadoSchema($version);
+    $returned_tables = $chado_schema->getBaseTables();
+
+    foreach ($known_tables as $table_name) {
+
+      $found = false;
+
+      foreach ($returned_tables as $check_table ){
+
+        if ($check_table == $table_name){
+          $found = True;
+        }
+      }
+      $this->assertTrue($found, "{$table_name} was not returned by getBaseTables for Chado v {$version}");
+    }
+
+  }
+
+  /**
+   * Tests ChadoSchema->getCvtermMapping() method.
+   *
+   * @dataProvider chadoTableProvider
+   *
+   * @group api
+   * @group chado
+   * @group chado-schema
+   */
+ // public function testGetCvtermMapping($version, $table_name) {
+
+    //
+//    // Ideally we would create a new chado table + mapping and then test this pulls it out
+//    // since admin can re-map terms. However, that's more then I meant to bite off right
+//    // now...
+//
+//    // @todo Test that known terms match the tables we expect.
+//
+//    // @todo Test that a non-existent term throws an error.
+//
+//    // @todo Test that an fake unmapped term returns no mapping.
+ // }
+
+  /**
+   * Data Provider: returns known tables specific to a given chado version.
+   *
+   * @return array
+   */
+   public function knownTableProvider() {
+    // chado version, array of 3 tables specific to version.
+
+    return [
+      ['1.2', ['cell_line_relationship', 'cvprop', 'chadoprop']],
+      ['1.3', ['analysis_cvterm', 'dbprop', 'organism_pub']],
+    ];
+   }
+
+  /**
+   * Data Provider: returns known tables specific to a given chado version.
+   *
+   * @return array
+   */
+   public function knownBaseTableProvider() {
+    // chado version, array of 3 tables specific to version.
+
+    return [
+      ['1.2', ['organism', 'feature', 'stock', 'project','analysis', 'phylotree']],
+      ['1.3', ['organism', 'feature', 'stock', 'project','analysis', 'phylotree']],
+    ];
+   }
+
+  /**
+   * Data Provider: returns known custom tables specific to a given chado version.
+   *
+   * NOTE: These tables are provided by core Tripal so we should be able to
+   *  depend on them. Also, for the same reason, chado version doesn't matter.
+   *
+   * @return array
+   */
+   public function knownCustomTableProvider() {
+
+    return [
+      ['library_feature_count'],
+      ['organism_feature_count'],
+      ['tripal_gff_temp'],
+    ];
+   }
+
+  /**
+   * DataProvider, a list of all chado tables.
+   *
+   * @return array
+   */
+  public function chadoTableProvider() {
+
+    // Provide the table list for all versions.
+    $dataset = [];
+    foreach (array('1.11','1.2','1.3') as $version) {
+      $chado_schema = new \ChadoSchema();
+      $version = $chado_schema->getVersion();
+
+      foreach ($chado_schema->getTableNames() as $table_name) {
+        $dataset[] = [$version, $table_name];
+      }
+    }
+
+    return $dataset;
+  }
+}

+ 119 - 0
tests/tripal_chado/example_files/test.obo

@@ -0,0 +1,119 @@
+format-version: 1.2
+default-namespace: tripal_obo_test
+ontology: tot
+subsetdef: test_normal "Normal nodes"
+subsetdef: test_crazy "Crazy nodes"
+
+[Term]
+id: TOT:001
+name: node01
+def: This is node 1
+subset: test_normal
+
+[Term]
+id: TOT:002
+name: node02
+is_a: TOT:001
+def: This is node 2
+subset: test_normal
+
+[Term]
+id: TOT:003
+name: node03
+is_a: TOT:001
+def: This is node 3
+subset: test_normal
+
+[Term]
+id: TOT:004
+name: node04
+is_a: TOT:001
+def: This is node 4
+subset: test_normal
+relationship: has_part TOT:011
+
+
+[Term]
+id: TOT:005
+name: node05
+is_a: TOT:001
+def: This is node 5
+subset: test_normal
+
+[Term]
+id: TOT:006
+name: node06
+is_a: TOT:003
+def: This is node 6
+subset: test_normal
+
+[Term]
+id: TOT:007
+name: node07
+is_a: TOT:003
+def: This is node 7
+subset: test_normal
+
+[Term]
+id: TOT:008
+name: node08
+is_a: TOT:007
+def: This is node 8
+subset: test_normal
+
+[Term]
+id: TOT:009
+name: node09
+is_a: TOT:004
+is_a: TOT:007
+def: This is node 9
+subset: test_normal
+
+[Term]
+id: TOT:010
+name: node10
+is_a: TOT:005
+def: This is node 10
+subset: test_normal
+
+[Term]
+id: TOT:011
+name: node11
+is_a: TOT:009
+is_a: TOT:010
+def: This is node 11 : Yo ! this comment should be ignored.
+synonym: "crazy node" EXACT []
+xref: GO:0043226
+subset: test_crazy
+comment: This is a crazy node!
+
+
+[Term]
+id: TOT:012
+name: node12
+is_a: TOT:010
+def: This is node 12
+subset: test_normal
+
+[Term]
+id: TOT:013
+name: node13
+is_a: TOT:011
+def: This is node 13
+subset: test_normal
+
+[Term]
+id: TOT:014
+name: node14
+is_obsolete: true
+def: This is node 14
+
+[Term]
+id: CHEBI:132502 ! fatty acid 18:3
+
+[Typedef]
+id: has_part
+name: has_part
+namespace: external
+xref: BFO:0000051
+is_transitive: true

+ 126 - 0
tests/tripal_chado/example_files/test.update.obo

@@ -0,0 +1,126 @@
+format-version: 1.2
+default-namespace: tripal_obo_test
+ontology: tot
+subsetdef: test_normal "Normal nodes"
+subsetdef: test_crazy "Crazy nodes"
+
+[Term]
+id: TOT:001
+name: node01
+def: This is node 1
+subset: test_normal
+
+[Term]
+id: TOT:003
+name: node03
+is_a: TOT:001
+def: This is node 3
+subset: test_normal
+
+[Term]
+id: TOT:004
+name: node04
+is_a: TOT:001
+def: This is node 4
+subset: test_normal
+relationship: has_part TOT:011
+
+
+[Term]
+id: TOT:005
+name: node05
+is_a: TOT:001
+def: This is node 5
+subset: test_normal
+
+[Term]
+id: TOT:006
+name: node06
+is_a: TOT:003
+def: This is node 6
+subset: test_normal
+
+[Term]
+id: TOT:007
+name: node07
+is_a: TOT:003
+def: This is node 7
+subset: test_normal
+
+[Term]
+id: TOT:008
+name: node08
+is_a: TOT:007
+def: This is node 8
+subset: test_normal
+is_obsolete: true
+
+[Term]
+id: TOT:009
+name: node09
+is_a: TOT:004
+is_a: TOT:007
+def: This is node 9
+subset: test_normal
+
+[Term]
+id: TOT:010
+name: node10
+is_a: TOT:005
+def: This is node 10
+subset: test_normal
+
+[Term]
+id: TOT:011
+name: node11
+is_a: TOT:009
+is_a: TOT:010
+def: This is node 11 : Yo ! this comment should be ignored.
+synonym: "crazy node" EXACT []
+xref: GO:0043226
+subset: test_crazy
+comment: This is a crazy node!
+
+
+[Term]
+id: TOT:012
+name: node12
+is_a: TOT:010
+def: This is node 12
+subset: test_normal
+
+[Term]
+id: TOT:013
+name: New name 13.
+is_a: TOT:011
+def: This is node 13
+subset: test_normal
+
+[Term]
+id: TOT:014
+name: node14
+is_obsolete: true
+def: This is node 14
+
+[Term]
+id: TOT:015
+name: node02
+def: This is a replacement for node 2
+is_a: TOT:001
+alt_id: TOT:002
+
+[Term]
+id: TOT:016
+name: node08
+def: This is a replacement for node 8
+is_a: TOT:007
+
+[Term]
+id: CHEBI:132502 ! fatty acid 18:3
+
+[Typedef]
+id: has_part
+name: has_part
+namespace: external
+xref: BFO:0000051
+is_transitive: true

+ 3 - 2
tests/tripal_chado/loaders/GFF3ImporterTest.php

@@ -123,6 +123,7 @@ class GFF3ImporterTest extends TripalTestCase {
    * The GFF importer should still create explicitly defined proteins if
    * skip_protein is true.
    *
+   * @group gff
    * @ticket 77
    */
   public function testGFFImporterLoadsExplicitProteins() {
@@ -176,11 +177,11 @@ class GFF3ImporterTest extends TripalTestCase {
 
   private function loadLandmarks($analysis, $organism) {
     $landmark_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/sequences/empty_landmarks.fasta'];
-
+    
     $run_args = [
       'organism_id' => $organism->organism_id,
       'analysis_id' => $analysis->analysis_id,
-      'seqtype' => 'scaffold',
+      'seqtype' => 'supercontig',
       'method' => 2, //default insert and update
       'match_type' => 1, //unique name default
       //optional

+ 606 - 0
tests/tripal_chado/loaders/OBOImporterTest.php

@@ -0,0 +1,606 @@
+<?php
+
+namespace Tests;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+
+class OBOImporterTest extends TripalTestCase {
+
+  // Uncomment to auto start and rollback db transactions per test method.
+  use DBTransaction;
+
+  /**
+   * A helper function for loading any OBO.
+   *
+   * @param $name - ontology name.  This goes in the tripal_cv_obo table.
+   * @param $path - path to the OBO.  this can be a file path or a URL.
+   *
+   * @throws \Exception
+   */
+  private function loadOBO($name, $path) {
+    
+    $obo_id = db_select('public.tripal_cv_obo', 't')
+    ->fields('t', ['obo_id'])
+    ->condition('t.name', $name)
+    ->execute()
+    ->fetchField();
+    
+    if (!$obo_id) {
+      
+      $obo_id = db_insert('public.tripal_cv_obo')
+      ->fields(['name' => $name, 'path' => $path])
+      ->execute();
+    }
+    
+    $run_args = ['obo_id' => $obo_id];
+    
+    module_load_include('inc', 'tripal_chado', 'includes/TripalImporter/OBOImporter');
+    $importer = new \OBOImporter();
+    $importer->create($run_args);
+    $importer->prepareFiles();
+    $importer->run();
+  }
+  
+  
+  /**
+   * Tests that an OBO from a remote URL can be loaded.  
+   * 
+   * For this test we will use the GO Plant Slim.
+   *
+   * @group obo
+   */
+   public function testRemoteOBO() {
+
+    $name = 'core_test_goslim_plant';
+    $path = 'http://www.geneontology.org/ontology/subsets/goslim_plant.obo';
+
+    $this->loadOBO($name, $path);
+
+    // Test that we get all three vocabularies added:  biological_process,
+    // cellular_component and molecular_function.
+    $bp_cv_id = db_select('chado.cv', 'c')
+      ->fields('c', ['cv_id'])
+      ->condition('name', 'biological_process')
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($bp_cv_id, 
+      "Missing the 'biological_process' cv record after loading the GO plant slim.");
+
+    $cc_cv_id = db_select('chado.cv', 'c')
+      ->fields('c', ['cv_id'])
+      ->condition('name', 'cellular_component')
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($cc_cv_id,
+      "Missing the 'cellular_component' cv record after loading the GO plant slim.");
+
+    $mf_cv_id = db_select('chado.cv', 'c')
+      ->fields('c', ['cv_id'])
+      ->condition('name', 'molecular_function')
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($mf_cv_id,
+      "Missing the 'molecular_function' cv record after loading the GO plant slim.");
+    
+    // Make sure we have a proper database record.
+    $go_db_id = db_select('chado.db', 'd')
+      ->fields('d', ['db_id'])
+      ->condition('name', 'GO')
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($go_db_id,
+      "Missing the 'GO' database record after loading the GO plant slim.");
+  } 
+  
+  /**
+   * Tests that an OBO from a local path can be loaded.
+   *
+   * For this test we will use a test ontology.
+   *
+   * @group obo
+   */
+  public function testLocalOBO() {
+    $name = 'tripal_obo_test';
+    $path = __DIR__ . '/../example_files/test.obo';
+    
+    $this->loadOBO($name, $path);
+    
+    // Make sure we have a proper vocabulary record.
+    $tot_cv_id = db_select('chado.cv', 'c')
+      ->fields('c', ['cv_id'])
+      ->condition('name', 'tripal_obo_test')
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($tot_cv_id,
+      "Missing the 'tripal_obo_test' cv record after loading the test.obo file");
+    
+    // Make sure we have a proper database record.
+    $tot_db_id = db_select('chado.db', 'd')
+      ->fields('d', ['db_id'])
+      ->condition('name', 'TOT')
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($tot_db_id,
+      "Missing the 'TOT' db record after loading the test.obo file");
+    
+    return [[$tot_cv_id, $tot_db_id]];
+  }
+
+  /**
+   * Test that all nodes in our test OBO are loaded.
+   * 
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+
+  public function testCVterms($cv_id, $db_id) {
+
+    // Our test OBO has 14 nodes.
+    $nodes = [
+      ['TOT:001' => 'node01'], 
+      ['TOT:002' => 'node02'], 
+      ['TOT:003' => 'node03'], 
+      ['TOT:004' => 'node04'], 
+      ['TOT:005' => 'node05'], 
+      ['TOT:006' => 'node06'],
+      ['TOT:007' => 'node07'], 
+      ['TOT:008' => 'node08'], 
+      ['TOT:009' => 'node09'],
+      ['TOT:010' => 'node10'], 
+      ['TOT:011' => 'node11'], 
+      ['TOT:012' => 'node12'], 
+      ['TOT:013' => 'node13'],
+      ['TOT:014' => 'node14'],
+    ];
+
+    // Test that the proper records were added to identify the term.    
+    foreach ($nodes as $id => $node_name) {
+      
+      // Check that cvterm record is inserted.
+      $cvterm_id = db_select('chado.cvterm', 'cvt')
+        ->fields('cvt', ['cvterm_id'])
+        ->condition('cvt.name', $node_name)
+        ->condition('cvt.cv_id', $cv_id)
+        ->execute()
+        ->fetchField();
+      $this->assertNotFalse($cvterm_id,
+        "Missing the cvterm record with name, '$node' after loading the test.obo file");
+      
+      // Check that the dbxref record is inserted.
+      $accession = preg_replace('/TOT:/', '', $id);
+      $dbxref_id = db_select('chado.dbxref', 'dbx')
+        ->fields('dbx', ['dbxref_id'])
+        ->condition('accession', $accession)
+        ->condition('db_id', $db_id);
+      $this->assertNotFalse($cvterm_id,
+        "Missing the dbxref record forid, '$id' after loading the test.obo file");
+    }
+    
+    // Test node 11 to make sure the definition was inserted correctly.
+    // The definition for node11 has an extra colon and a comment.  The colon
+    // should not throw off the insertion of the full definition and
+    // the comment should be excluded.
+    $def = db_select('chado.cvterm', 'cvt')
+      ->fields('cvt', ['definition'])
+      ->condition('cvt.name', 'node11')
+      ->condition('cvt.cv_id', $cv_id)
+      ->execute()
+      ->fetchField();
+    $this->assertNotFalse($def,
+      "The definition for node11 was not added.");
+    $this->assertEquals('This is node 11 : Yo', $def,
+      "The definition for node11 is incorrect. it was stored as \"$def\" but should be \"def: This is node 11 : Yo\".");
+    
+    // Make sure that colons in term names don't screw up the term. This test
+    // corresponds to the term with id CHEBI:132502 in the test.obo file.
+    $exists = db_select('chado.cv', 'c')
+      ->fields('c', ['cv_id'])
+      ->condition('name', 'fatty acid 18')
+      ->execute()
+      ->fetchField();
+    $this->assertFalse($exists);
+    
+    
+    // Node14 should be marked as obsolete.
+    $sql = "
+      SELECT CVT.is_obsolete
+      FROM {cvterm} CVT
+         INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN {db} DB on DB.db_id = DBX.db_id
+       WHERE DB.name = 'TOT' and DBX.accession = '014'
+    ";
+    $is_obsolete = chado_query($sql)->fetchField();
+    $this->assertEquals(1, $is_obsolete,
+      "The term, node14, should be marked as obsolete after loading of the test.obo file.");
+    
+    // Every vocabulary should have an is_a term added to support the is_a
+    // relationships.
+    $sql = "
+      SELECT CVT.is_relationshiptype
+      FROM {cvterm} CVT
+         INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN {db} DB on DB.db_id = DBX.db_id
+       WHERE CVT.name = 'is_a' and DB.name = 'TOT'
+    ";
+    $is_reltype = chado_query($sql)->fetchField();
+    $this->assertNotFalse($is_reltype,
+      "The cvterm record for, is_a, should have been added during loading of the test.obo file.");
+    $this->assertEquals(1, $is_reltype, 
+      "The cvterm record, is_a, should be marked as a relationship type.");
+    
+  }
+  
+  /**
+   * Test that insertion of synonyms works.
+   * 
+   * The term 'node11' has a synonym:"crazy node" EXACT []
+   * 
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testSynonyms($cv_id, $db_id){
+     
+    $query = db_select('chado.cvtermsynonym', 'cvts');
+    $query->fields('cvts', ['synonym']);
+    $query->join('chado.cvterm', 'cvt', 'cvts.cvterm_id = cvt.cvterm_id');
+    $query->condition('cvt.name', 'node11');
+    $synonym = $query->execute()->fetchField();
+    $this->assertNotFalse($synonym,
+      "Failed to find the 'crazy node' synonym record for node 11 after loading the test.obo file.");
+    
+    $this->assertEquals("crazy node", $synonym,
+      "Failed to properly add the 'crazy node' synonym for node 11 instead the following was loaded: $synonym");
+  }
+  
+  /**
+   * Test that insertion of subset works.
+   *
+   * The term 'node11' belongs to the test_crazy subset. Everything else belongs
+   * to the test_normal subset.
+   * 
+   *
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testSubset($cv_id, $db_id) {
+    
+    $sql = "
+      SELECT CVT.name
+      FROM {cvtermprop} CVTP
+        INNER JOIN {cvterm} CVTPT on CVTPT.cvterm_id = CVTP.type_id
+        INNER JOIN {cvterm} CVT on CVT.cvterm_id = CVTP.cvterm_id
+        INNER JOIN {dbxref} DBX on CVT.dbxref_id = DBX.dbxref_id
+        INNER JOIN {db} DB on DB.db_id = DBX.db_id
+      WHERE CVTPT.name = 'Subgroup' and DB.name = 'TOT' and CVTP.value = 'test_crazy'
+    ";
+    $term_name = chado_query($sql)->fetchField();
+    $this->assertNotFalse($term_name,
+      "This cvtermprop record for the subset 'test_crazy' is missing.");
+    
+    $this->assertEquals('node11', $term_name,
+      "This cvtermprop record for the subset 'test_crazy' is assigned to term, $term_name, instead of node11.");
+  
+    $sql = "
+      SELECT count(CVT.cvterm_id)
+      FROM {cvtermprop} CVTP
+        INNER JOIN {cvterm} CVTPT on CVTPT.cvterm_id = CVTP.type_id
+        INNER JOIN {cvterm} CVT on CVT.cvterm_id = CVTP.cvterm_id
+        INNER JOIN {dbxref} DBX on CVT.dbxref_id = DBX.dbxref_id
+        INNER JOIN {db} DB on DB.db_id = DBX.db_id
+      WHERE CVTPT.name = 'Subgroup' and DB.name = 'TOT' and CVTP.value = 'test_normal'
+    ";
+    $subset_count = chado_query($sql)->fetchField();
+    
+    $this->assertNotFalse($subset_count,
+      "This cvtermprop record for the subset 'test_normal' are missing.");
+    
+    // There should be 12 terms that belong to subset 'test_normal' as node14
+    // does not belong to a subset.
+    $this->assertEquals(12, $subset_count,
+      "There are $subset_count cvtermprop record for the subset 'test_normal' but there should be 13.");
+  }
+  
+  /**
+   * Test that the insertion of xref works.
+   *
+   * The term 'node11' belongs to the test_crazy subset. Everything else belongs
+   * to the test_normal subset.
+   *
+   *
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testXref($cv_id, $db_id) {
+    
+    $sql = "
+      SELECT concat(DB2.name, ':', DBX2.accession)
+      FROM {cvterm} CVT
+        INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+        INNER JOIN {db} on DB.db_id = DBX.db_id
+        INNER JOIN {cvterm_dbxref} CVTDBX on CVTDBX.cvterm_id = CVT.cvterm_id
+        INNER JOIN {dbxref} DBX2 on DBX2.dbxref_id = CVTDBX.dbxref_id
+        INNER JOIN {db} DB2 on DB2.db_id = DBX2.db_id
+      WHERE DB.name = 'TOT' and CVT.name = 'node11'
+      ORDER BY DBX.accession
+    ";
+    $xref_id = chado_query($sql)->fetchField();
+    $this->assertNotFalse($xref_id,
+      "This cvterm_dbxref record for the xref 'GO:0043226' is missing for node11.");
+    
+    $this->assertEquals('GO:0043226', $xref_id,
+      "This cvterm_dbxref record for node 11 is, $xref_id, instead of GO:0043226.");
+  }
+  
+  /**
+   * Test that the insertion of comments works.
+   *
+   * The term 'node11' contains a comment.
+   *
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testComment($cv_id, $db_id) {
+    
+    $sql = "
+      SELECT CVTP.value
+      FROM {cvterm} CVT
+        INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+        INNER JOIN {db} on DB.db_id = DBX.db_id
+        INNER JOIN {cvtermprop} CVTP on CVTP.cvterm_id = CVT.cvterm_id
+        INNER JOIN {cvterm} CVTPT on CVTPT.cvterm_id = CVTP.type_id
+      WHERE DB.name = 'TOT' and CVTPT.name = 'comment' and CVT.name = 'node11'
+      ORDER BY DBX.accession
+    ";
+    $comment = chado_query($sql)->fetchField();
+    $this->assertNotFalse($xref_id,
+      "This cvterm_dbxref record for the xref 'This is a crazy node' is missing for node11.");
+    
+    $this->assertEquals('This is a crazy node', $comment,
+      "This cvterm_dbxref record for node11 is, \"$comment\", instead of \"This is a crazy node\".");
+  }
+  
+  /**
+   * Tests that the cvtermpath is properly loaded.
+   *
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testRelationships($cv_id, $db_id) {
+    $relationships = [
+      ['node02', 'is_a', 'node01'],
+      ['node03', 'is_a', 'node01'],
+      ['node04', 'is_a', 'node01'],
+      ['node04', 'has_part', 'node11'],
+      ['node05', 'is_a', 'node01'],
+      ['node06', 'is_a', 'node03'],
+      ['node07', 'is_a', 'node03'],
+      ['node08', 'is_a', 'node07'],
+      ['node09', 'is_a', 'node04'],
+      ['node09', 'is_a', 'node07'],
+      ['node10', 'is_a', 'node05'],
+      ['node11', 'is_a', 'node09'],
+      ['node11', 'is_a', 'node10'],
+      ['node12', 'is_a', 'node10'],
+      ['node13', 'is_a', 'node11'],
+    ];
+    foreach ($relationships  as $relationship) {
+      $subject = $relationship[0];
+      $type = $relationship[1];
+      $object = $relationship[2];
+      $sql = "
+        SELECT CVTR.cvterm_relationship_id
+        FROM {cvterm} CVT
+          INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+          INNER JOIN {db} on DB.db_id = DBX.db_id
+          INNER JOIN {cvterm_relationship} CVTR on CVTR.subject_id = CVT.cvterm_id
+          INNER JOIN {cvterm} CVT2 on CVT2.cvterm_id = CVTR.object_id
+          INNER JOIN {cvterm} CVT3 on CVT3.cvterm_id = CVTR.type_id
+        WHERE DB.name = 'TOT' AND CVT2.name = :object AND 
+          CVT3.name = :type AND CVT.name = :subject
+      ";
+      $args = [':object' => $object, ':type' => $type, ':subject' => $subject];
+      $rel_id = chado_query($sql, $args)->fetchField();
+      $this->assertNotFalse($rel_id,
+        "The following relationship could not be found: $subect $type $object.");
+    }
+    
+    // Now make sure we have no more relationships than what we are supposed
+    // to have.
+    $sql = "
+      SELECT count(CVTR.cvterm_relationship_id)
+      FROM {cvterm} CVT
+        INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+        INNER JOIN {db} on DB.db_id = DBX.db_id
+        INNER JOIN {cvterm_relationship} CVTR on CVTR.object_id = CVT.cvterm_id
+      WHERE DB.name = 'TOT' AND CVT.is_relationshiptype = 0
+    ";
+    $rel_count = chado_query($sql)->fetchField();
+    $expected = count($relationships);
+    $this->assertEquals($expected, $rel_count,
+      "There are an incorrect number of relationships. There were $rel_count found but there should be $expected.");
+  }
+
+  /**
+   * Tests that the cvtermpath is properly loaded.
+   * 
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testCVtermPath($cv_id, $db_id) {
+    
+    // For now we won't include distance or type in the check because depending
+    // how the tree was loaded and if there are multiple paths to a node
+    // then there's no guarantee we'll always get the same path. Therefore the
+    // type and pathdistance may be different (althoug not incorrect).
+    $relationships = [
+      // Node01 as root: note that the root term always has a link to itself
+      // in the cvtermpath table.
+      ['node01', 'node01'],
+      ['node01', 'node02'],
+      ['node01', 'node03'],
+      ['node01', 'node04'],
+      ['node01', 'node05'],
+      ['node01', 'node06'],
+      ['node01', 'node07'],
+      ['node01', 'node08'],
+      ['node01', 'node09'],
+      ['node01', 'node10'],
+      ['node01', 'node11'],
+      ['node01', 'node12'],
+      ['node01', 'node13'],
+      // Node03 as root.
+      ['node03', 'node04'], 
+      ['node03', 'node06'],
+      ['node03', 'node07'],
+      ['node03', 'node08'],
+      ['node03', 'node09'],
+      ['node03', 'node11'],
+      ['node03', 'node13'],
+      // Node04 as root.
+      ['node04', 'node09'],
+      ['node04', 'node11'],
+      ['node04', 'node13'],
+      // Node05 as root.
+      ['node05', 'node04'],
+      ['node05', 'node09'],
+      ['node05', 'node10'],
+      ['node05', 'node11'],
+      ['node05', 'node12'],
+      ['node05', 'node13'],
+      // Node07 as root.
+      ['node07', 'node04'],
+      ['node07', 'node08'],
+      ['node07', 'node09'],
+      ['node07', 'node11'],
+      ['node07', 'node13'],
+      // Node09 as root.
+      ['node09', 'node04'],
+      ['node09', 'node11'],
+      ['node09', 'node13'],
+      // Node10 as root.
+      ['node10', 'node04'],
+      ['node10', 'node09'],
+      ['node10', 'node11'],
+      ['node10', 'node12'],
+      ['node10', 'node13'],
+      // Node11 as root.
+      ['node11', 'node04'],
+      ['node11', 'node09'],
+      ['node11', 'node13'],
+    ];
+
+    // Populate the cvtermpath for our test OBO.
+    chado_update_cvtermpath($cv_id);
+    
+    foreach ($relationships as $relationship) {
+      $object = $relationship[0];
+      $subject = $relationship[1];
+      $sql = "
+        SELECT cvtermpath_id
+        FROM {cvtermpath} CVTP 
+          INNER JOIN {cvterm} CVTO on CVTO.cvterm_id = CVTP.object_id
+          INNER JOIN {cvterm} CVTS on CVTS.cvterm_id = CVTP.subject_id
+          INNER JOIN {cvterm} CVTT on CVTT.cvterm_id = CVTP.type_id
+        WHERE CVTP.cv_id = :cv_id and CVTO.name = :object and 
+          CVTS.name = :subject
+      ";
+      $args = [':cv_id' => $cv_id, ':object' => $object, ':subject' => $subject];
+      $cvtermpath_id = chado_query($sql, $args)->fetchField();
+      $this->assertNotFalse($cvtermpath_id,
+        "Cound not find the cvtermpath record for the relationship: $subject => $object.");
+    }
+
+    // Now make sure we have no additional entries.
+    $sql = "
+          SELECT count(cvtermpath_id)
+          FROM {cvtermpath} CVTP
+            INNER JOIN {cvterm} CVTO on CVTO.cvterm_id = CVTP.object_id
+            INNER JOIN {cvterm} CVTS on CVTS.cvterm_id = CVTP.subject_id
+            INNER JOIN {cvterm} CVTT on CVTT.cvterm_id = CVTP.type_id
+          WHERE CVTP.cv_id = :cv_id
+        ";
+    $args = [':cv_id' => $cv_id];
+    $rel_count = chado_query($sql, $args)->fetchField();
+    $expected = count($relationships);
+    $this->assertEquals($expected, $rel_count,
+      "There are an incorrect number of paths. There were $rel_count found but there should be $expected.");
+  }
+  
+  /**
+   * Tests that the EBI Lookup is properly working.
+   * 
+   * The term CHEBI:132502 should have been loaded via EBI.
+   * 
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testEBILookup($cv_id, $db_id) {
+    $sql = "
+       SELECT CVT.cvterm_id
+       FROM  {cvterm} CVT
+         INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN {db} DB on DB.db_id = DBX.db_id
+       WHERE DB.name = 'CHEBI' and DBX.accession = '132502'
+    ";
+    $cvterm_id = chado_query($sql)->fetchField();
+    $this->assertNotFalse($cvterm_id,
+      "The term, CHEBI:132502, is not present the EBI OLS lookup must not have succeeded.");
+  }
+  
+  /**
+   * Tests when changes are made between OBO loads.
+   *
+   * Sometimes an ontology can change the names of it's terms, or set some
+   * as obsolete, etc. We need to makes sure that when changes are made and
+   * the OBO is reloaded that the terms are properly update.
+   *
+   * @group obo
+   * @dataProvider testLocalOBO
+   */
+  public function testOBOChanges($cv_id, $db_id) {
+    $name = 'tripal_obo_test_update';
+    $path = __DIR__ . '/../example_files/test.update.obo';
+    
+    $this->loadOBO($name, $path);
+    
+    // Did the name of term 13 change?
+    $sql = "
+      SELECT CVT.name
+      FROM {cvterm} CVT
+         INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN {db} DB on DB.db_id = DBX.db_id
+       WHERE DB.name = 'TOT' and DBX.accession = '013'
+    ";
+    $name = chado_query($sql)->fetchField();
+    $this->assertEquals('New name 13.', $name,
+      "The name for node13 (TOT:013) failed to update to 'New name 13'.");
+    
+    // Node15 is new, and node02 got removed. Node15 now uses node02's name and
+    // has TOT:002 as an alt_id. So, node02 should be marked as obsolete
+    $sql = "
+      SELECT CVT.is_obsolete
+      FROM {cvterm} CVT
+         INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN {db} DB on DB.db_id = DBX.db_id
+       WHERE DB.name = 'TOT' and DBX.accession = '002'
+    ";
+    $is_obsolete = chado_query($sql)->fetchField();
+    $this->assertEquals(1, $is_obsolete,
+      "The node02 (TOT:002) should be marked as obsolete after update.");
+    
+    // Node16 is new, and node08 is now obsolete. Node16 now uses node08's name,
+    // so, node08 should be marked as obsolete and have the word '(obsolete)'
+    // added to prevent future conflicts.
+    $sql = "
+      SELECT CVT.name
+      FROM {cvterm} CVT
+         INNER JOIN {dbxref} DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN {db} DB on DB.db_id = DBX.db_id
+       WHERE DB.name = 'TOT' and DBX.accession = '008'
+    ";
+    $name = chado_query($sql)->fetchField();
+    $this->assertEquals("node08 (obsolete)", $name,
+      "The node08 (TOT:008) should be marked as obsolete after update.");
+  }
+}

+ 4 - 4
tripal/api/tripal.DEPRECATED.api.inc

@@ -32,10 +32,10 @@
  */
 function tripal_get_job_end($job) {
   tripal_report_error('tripal_deprecated', TRIPAL_NOTICE,
-    "DEPRECATED: %function has been removed from the API the end date " .
+    "DEPRECATED: %old_function has been removed from the API the end date " .
     "is now accessible via the %property property. Please update your code.",
     array(
-      '%old_function' => 'tripal_jobs_get_end_time',
+      '%old_function' => 'tripal_get_job_end',
       '%property' => '\$job->end_time_string',
     )
   );
@@ -66,7 +66,7 @@ function tripal_get_job_end($job) {
 function tripal_get_job_start($job) {
 
   tripal_report_error('tripal_deprecated', TRIPAL_NOTICE,
-    "DEPRECATED: %function has been removed from the API the end date " .
+    "DEPRECATED: %old_function has been removed from the API the end date " .
     "is now accessible via the %property property. Please update your code.",
     array(
       '%old_function' => 'tripal_get_job_start',
@@ -104,7 +104,7 @@ function tripal_get_job_start($job) {
 function tripal_get_job_submit_date($job) {
 
   tripal_report_error('tripal_deprecated', TRIPAL_NOTICE,
-      "DEPRECATED: %function has been removed from the API the end date " .
+      "DEPRECATED: %old_function has been removed from the API the end date " .
       "is now accessible via the %property property. Please update your code.",
       array(
         '%old_function' => 'tripal_get_job_submit_date',

+ 0 - 2
tripal/api/tripal.notice.api.inc

@@ -63,8 +63,6 @@ define('TRIPAL_DEBUG',7);
  *   An array of options. Some available options include:
  *     - print: prints the error message to the terminal screen. Useful when
  *       display is the command-line
- *     - drupal_set_message: set to TRUE to call drupal_set_message with the 
- *       same error.
  *     - drupal_set_message:  set to TRUE then send the message to the
  *       drupal_set_message function.
  *     - watchdog:  set to FALSE to disable logging to Drupal's watchdog.

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

@@ -155,7 +155,6 @@ function hook_vocab_get_term($vocabulary, $accession) {
 function hook_vocab_get_terms($vocabulary, $limit = 25, $element = 0) {
   // See the tripal_chado_vocab_get_terms() function for an example.
 }
-
 /**
  * Hook used by the default term storage backend to provide children for a term.
  *
@@ -423,7 +422,6 @@ function tripal_get_vocabulary_root_terms($vocabulary) {
     }
   }
 }
-
 /**
  * Retrieves the immediate children of the given term.
  *

+ 5 - 1
tripal/includes/TripalFieldDownloaders/TripalCSVDownloader.inc

@@ -54,7 +54,7 @@ class TripalCSVDownloader extends TripalFieldDownloader {
       $field_name = $field['field_name'];
 
       // If we only have one item for this value then add it.
-      if (is_array($entity->{$field_name}['und']) and count($entity->{$field_name}['und']) == 1) {
+      if ($field_name and is_array($entity->{$field_name}['und']) and count($entity->{$field_name}['und']) == 1) {
         $value = $entity->{$field_name}['und'][0]['value'];
 
         // If the single element is not an array then this is good.
@@ -80,6 +80,10 @@ class TripalCSVDownloader extends TripalFieldDownloader {
           // TODO: What to do with fields that are arrays?
         }
       }
+      // Report the misconfigured field
+      elseif(!$field_name) {
+        tripal_report_error('tripal', TRIPAL_ERROR, 'Unable to find field name for field id: '.$field_id);
+      }
       // If we have multiple items then deal with that.
       else {
         $col[] = '';

+ 5 - 1
tripal/includes/TripalFieldDownloaders/TripalTabDownloader.inc

@@ -54,7 +54,7 @@ class TripalTabDownloader extends TripalFieldDownloader {
        $field_name = $field['field_name'];
 
        // If we only have one item for this value then add it.
-       if (is_array($entity->{$field_name}['und']) and count($entity->{$field_name}['und']) == 1) {
+       if ($field_name and is_array($entity->{$field_name}['und']) and count($entity->{$field_name}['und']) == 1) {
          $value = $entity->{$field_name}['und'][0]['value'];
 
          // If the single element is not an array then this is good.
@@ -76,6 +76,10 @@ class TripalTabDownloader extends TripalFieldDownloader {
            // TODO: What to do with fields that are arrays?
          }
        }
+       // Report the misconfigured field
+       elseif(!$field_name) {
+         tripal_report_error('tripal', TRIPAL_ERROR, 'Unable to find field name for field id: '.$field_id);
+       }
        // If we have multiple items then deal with that.
        else {
          $col[] = '';

+ 19 - 14
tripal/includes/TripalImporter.inc

@@ -204,6 +204,11 @@ class TripalImporter {
   protected $is_prepared;
 
 
+  /**
+   * Stores the last percentage that progress was reported.
+   * @var integer
+   */
+  protected $reported = 0;
 
   // --------------------------------------------------------------------------
   //                          CONSTRUCTORS
@@ -632,37 +637,37 @@ class TripalImporter {
 
     if ($total_handled == 0) {
       $memory = number_format(memory_get_usage());
-      print "Percent complete: 0%. Memory: " . $memory . " bytes.\r";
+      print t("Percent complete: 0%. Memory: !memory  bytes.", ['!memory' => $memory]) . "\r";
       return;
     }
 
     // Now see if we need to report to the user the percent done.  A message
     // will be printed on the command-line if the job is run there.
-    $percent = sprintf("%.2f", ($this->num_handled / $this->total_items) * 100);
-    $diff = $percent - $this->prev_update;
-
-    if ($diff >= $this->interval) {
-
+    $percent = ($this->num_handled / $this->total_items) * 100;
+    $ipercent  = (int) $percent;
+    
+    // If we've reached our interval then print update info.
+    if ($ipercent > 0 and $ipercent != $this->reported and $ipercent % $this->interval == 0) {
       $memory = number_format(memory_get_usage());
-      print "Percent complete: " . $percent . "%. Memory: " . $memory . " bytes.\r";
+      $spercent = sprintf("%.2f", $percent);
+      print t("Percent complete: !percent %. Memory: !memory bytes.", 
+        ['!percent' => $spercent, '!memory' => $memory]) . "\r";
 
       // If we have a job the update the job progress too.
       if ($this->job) {
         $this->job->setProgress($percent);
       }
-
-      $this->prev_update = $diff;
+      $this->reported = $ipercent;
     }
   }
 
   /**
    * Updates the percent interval when the job progress is updated.
    *
-   * Updating the job
-   * progress incurrs a database write which takes time and if it occurs to
-   * frequently can slow down the loader.  This should be a value between
-   * 0 and 100 to indicate a percent interval (e.g. 1 means update the
-   * progress every time the num_handled increases by 1%).
+   * Updating the job progress incurrs a database write which takes time 
+   * and if it occurs to frequently can slow down the loader.  This should 
+   * be a value between 0 and 100 to indicate a percent interval (e.g. 1 
+   * means update the progress every time the num_handled increases by 1%).
    *
    * @param $interval
    *   A number between 0 and 100.

+ 38 - 14
tripal/includes/TripalJob.inc

@@ -33,13 +33,27 @@ class TripalJob {
    * 0 and 100 to indicate a percent interval (e.g. 1 means update the
    * progress every time the num_handled increases by 1%).
    */
-  private $interval;
-
+  private $interval;  
+  
+  /**
+   * The time stamp when the job begins.
+   * 
+   * @var integer
+   */
+  private $start_time;
+  
+  /**
+   * The time from when the setTotalItems is called to the present time.
+   * 
+   * @var
+   */
+  private $progress_start_time;
+  
   /**
-   * Each time the job progress is updated this variable gets set.  It is
-   * used to calculate if the $interval has passed for the next update.
+   * Stores the last percentage that progress was reported.
+   * @var integer
    */
-  private $prev_update;
+  private $reported = 0;
 
   /**
    * Instantiates a new TripalJob object.
@@ -277,6 +291,9 @@ class TripalJob {
    * Executes the job.
    */
   public function run() {
+    
+    $this->start_time = time();
+    $this->progress_start_time = time();
 
     if (!$this->job) {
       throw new Exception('Cannot launch job as no job is associated with this object.');
@@ -296,7 +313,7 @@ class TripalJob {
       // Set the start time for this job.
       $record = new stdClass();
       $record->job_id = $this->job->job_id;
-      $record->start_time = time();
+      $record->start_time = $this->start_time;
       $record->status = 'Running';
       $record->pid = getmypid();
       drupal_write_record('tripal_jobs', $record, 'job_id');
@@ -447,6 +464,8 @@ class TripalJob {
    *   The total number of items to process.
    */
   public function setTotalItems($total_items) {
+    $this->progress_start_time = time();
+    
     $this->total_items = $total_items;
   }
 
@@ -472,6 +491,7 @@ class TripalJob {
    *   The total number of items that have been processed.
    */
   public function setItemsHandled($total_handled) {
+    
     // First set the number of items handled.
     $this->num_handled = $total_handled;
 
@@ -483,15 +503,19 @@ class TripalJob {
 
     // Now see if we need to report to the user the percent done.  A message
     // will be printed on the command-line if the job is run there.
-    $percent = sprintf("%.2f", ($this->num_handled / $this->total_items) * 100);
-    $diff = $percent - $this->prev_update;
-
-    if ($diff >= $this->interval) {
-
-      $memory = number_format(memory_get_usage());
-      print "Percent complete: " . $percent . "%. Memory: " . $memory . " bytes.\r";
-      $this->prev_update = $diff;
+    $percent = ($this->num_handled / $this->total_items) * 100;
+    $ipercent = (int) $percent;
+
+    // If we've reached our interval then print update info.
+    if ($ipercent > 0 and $ipercent != $this->reported and ($ipercent % $this->interval) == 0) {
+      $duration = (time() - $this->progress_start_time) / 60;
+      $duration = sprintf("%.2f", $duration);
+      $memory = memory_get_usage();
+      $fmemory = number_format($memory);
+      $spercent = sprintf("%d", $percent);
+      print "Percent complete: " . $spercent . "%. Memory: " . $fmemory . " bytes. Duration: " . $duration . " mins\r";
       $this->setProgress($percent);
+      $this->reported = $ipercent;
     }
   }
 

+ 2 - 2
tripal/includes/tripal.jobs.inc

@@ -347,7 +347,7 @@ function tripal_jobs_view($job_id) {
     '#collapsed' => TRUE,
     '#collapsible' => TRUE,
     '#attributes' => array(
-      'class' => array('collapsible', 'collapsed'),
+      'class' => array('collapsible'),
     ),
     '#attached' => array(
       'js' => array('misc/collapse.js', 'misc/form.js')
@@ -363,7 +363,7 @@ function tripal_jobs_view($job_id) {
     '#collapsed' => TRUE,
     '#collapsible' => TRUE,
     '#attributes' => array(
-      'class' => array('collapsible', 'collapsed'),
+      'class' => array('collapsible'),
     ),
     '#attached' => array(
       'js' => array('misc/collapse.js', 'misc/form.js')

+ 140 - 107
tripal/includes/tripal.term_lookup.inc

@@ -59,6 +59,8 @@ function tripal_vocabulary_lookup_vocab_page($vocabulary) {
   drupal_set_breadcrumb($breadcrumb);
 
   $vocab = tripal_get_vocabulary_details($vocabulary);
+  $vocab_table = tripal_vocabulary_get_vocab_details($vocab);
+  
   if ($vocab['description']) {
     drupal_set_title($vocabulary . ': ' . $vocab['description']);
   }
@@ -71,58 +73,7 @@ function tripal_vocabulary_lookup_vocab_page($vocabulary) {
     drupal_set_message('The vocabulary cannot be found on this site', 'error');
     return '';
   }
-
-  $headers = array();
-  $rows = array();
-  $vocab_name = $vocab['name'];
-  $short_name = $vocab['short_name'];
-  if ($vocab['url']) {
-    $short_name = l($vocab['short_name'], $vocab['url'], array('attributes' => array('target' => '_blank')));
-  }
-  $vocab_desc = $vocab['description'];
-  $rows[] = array(
-    array(
-      'data' => 'Short Name',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $short_name,
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Vocabulary Name(s)',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $vocab_name,
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Description',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $vocab_desc,
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Number of Terms Loaded on This Site',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    number_format($vocab['num_terms']),
-  );
-
-  $table = array(
-    'header' => $headers,
-    'rows' => $rows,
-    'attributes' => array(),
-    'sticky' => FALSE,
-    'caption' => '',
-    'colgroups' => array(),
-    'empty' => '',
-  );
-
+  
   $has_root = TRUE;
   $root_terms = tripal_get_vocabulary_root_terms($vocabulary);
   // If this vocabulary doesn't have root terms then it's either not an
@@ -156,7 +107,9 @@ function tripal_vocabulary_lookup_vocab_page($vocabulary) {
     'vocab_table' => array(
       '#type' => 'item',
       '#title' => 'Details',
-      '#markup' => '<p>A vocabulary is always identified by its short name and sometimes it may offer multiple sub-vocabularies with different names. Both are listed below.</p>' . theme_table($table),
+      '#markup' => '<p>A vocabulary is always identified by its short name ' . 
+        'and sometimes it may offer multiple sub-vocabularies with different ' . 
+        'names. Both are listed below.</p>' . $vocab_table,
     ),
     'vocab_browser' => array(
       '#type' => 'item',
@@ -178,6 +131,69 @@ function tripal_vocabulary_lookup_vocab_page($vocabulary) {
   return $content;
 }
 
+/**
+ * Generates a table view of the vocabulary.
+ * 
+ * @param $vocab
+ *   The vocabulary array.
+ * @return 
+ *    An HTML rendered table describing the vocabulary.
+ */
+function tripal_vocabulary_get_vocab_details($vocab) {
+  $headers = array();
+  $rows = array();
+  $vocab_name = $vocab['name'];
+  $short_name = $vocab['short_name'];
+  if ($vocab['url']) {
+    $short_name = l($vocab['short_name'], $vocab['url'], array('attributes' => array('target' => '_blank')));
+  }
+  $vocab_desc = $vocab['description'];
+  $rows[] = array(
+    array(
+      'data' => 'Short Name',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    $short_name,
+  );
+  $rows[] = array(
+    array(
+      'data' => 'Vocabulary Name(s)',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    $vocab_name,
+  );
+  $rows[] = array(
+    array(
+      'data' => 'Description',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    $vocab_desc,
+  );
+  $rows[] = array(
+    array(
+      'data' => 'Number of Terms Loaded on This Site',
+      'header' => TRUE,
+      'width' => '20%',
+    ),
+    number_format($vocab['num_terms']),
+  );
+  
+  $table = array(
+    'header' => $headers,
+    'rows' => $rows,
+    'attributes' => array(),
+    'sticky' => FALSE,
+    'caption' => '',
+    'colgroups' => array(),
+    'empty' => '',
+  );
+  
+  return theme_table($table);
+}
+
 /**
  * A helper function to format an array of terms into a list for the web page.
  *
@@ -203,9 +219,6 @@ function tripal_vocabulary_lookup_term_children_format($children) {
     if ($child['accession'] != $child['name']) {
       $items .= ' [' . $child['vocabulary']['short_name'] . ':' . $child['accession'] . '] ';
     }
-    if ($num_grand > 0) {
-      $items .= ' (' . $num_grand . ')';
-    }
     $items .= '</li>';
   }
   $items .= '</ul>';
@@ -257,9 +270,10 @@ function tripal_vocabulary_lookup_term_page($vocabulary, $accession) {
   $breadcrumb[] = l($vocabulary, 'cv/lookup/' . $vocabulary);
   drupal_set_breadcrumb($breadcrumb);
 
+  $vocab = tripal_get_vocabulary_details($vocabulary);
+  $vocab_table = tripal_vocabulary_get_vocab_details($vocab);
+  
   $term = tripal_get_term_details($vocabulary, $accession);
-  drupal_set_title($term['name']);
-
 
   // If we can't find the term then just return a message.
   if (!$term) {
@@ -272,8 +286,12 @@ function tripal_vocabulary_lookup_term_page($vocabulary, $accession) {
   $rows = array();
   $term_name = $term['name'];
   $accession = $term['vocabulary']['short_name'] . ':' . $term['accession'];
-  if ($term['url']) {
-    $term_name = l($term['name'], $term['url'], array('attributes' => array('target' => '_blank')));
+  drupal_set_title($accession);
+  
+  // Create a URL to point this term to it's source page, but only if the
+  // source is not this site.
+  if ($term['url'] and !preg_match('/cv\/lookup/', $term['url'])) {
+    $accession = l($accession, $term['url'], array('attributes' => array('target' => '_blank')));
   }
   $rows[] = array(
     array(
@@ -299,63 +317,78 @@ function tripal_vocabulary_lookup_term_page($vocabulary, $accession) {
     ),
     $term['definition'],
   );
-
-
-  $table = array(
-    'header' => $headers,
-    'rows' => $rows,
-    'attributes' => array(),
-    'sticky' => FALSE,
-    'caption' => 'Term Details',
-    'colgroups' => array(),
-    'empty' => '',
-  );
-  $content = theme_table($table);
-
-
-  $rows = array();
-  $vocab_name = $term['vocabulary']['name'];
-  if ($term['vocabulary']['url']) {
-    $vocab_name = l($term['vocabulary']['name'], $term['vocabulary']['url'], array('attributes' => array('target' => '_blank')));
+  
+  // Now iterate through any other columns in the term array and add those
+  // details.
+  foreach ($term as $key => $value) {
+    if (in_array($key, ['name', 'definition', 'vocabulary', 'accession', 'url'])) {
+      continue;
+    }
+    // Convert thisto an array so we can alter it.
+    if (!is_array($value)) {
+      $new_values[] = $value;
+      $value = $new_values;
+    }
+    // If this is a relationship key then let's try to rewrite the GO 
+    // term in the relationship as a link.
+    if ($key == 'relationship') {
+      foreach ($value as $index => $v) {
+        $matches = [];
+        if (preg_match('/^(.+)\s(.+?):(.+?)$/', $v, $matches)) {
+          $rel = $matches[1];
+          $voc = $matches[2];
+          $acc = $matches[3];
+          $v = $rel . ' ' . l($voc . ':' . $acc, 'cv/lookup/' . $voc . '/' . $acc, ['attributes' => ['target' => '_blank']]);
+          $t = tripal_get_term_details($voc, $acc);
+          if ($t) {
+            $v .= ' (' . $t['name'] . ')';
+          }
+          $value[$index] = $v;
+        }
+      }
+    }
+    if (count($value) > 1) {
+      $value_str = theme_item_list([
+        'items' => $value,
+        'type' => 'ul',
+        'attributes' => [],
+        'title' => '',
+      ]);
+    }
+    else {
+      $value_str = $value[0];
+    }
+    $rows[] = array(
+      array(
+        'data' => ucfirst($key),
+        'header' => TRUE,
+        'width' => '20%',
+      ),
+      $value_str,
+    );
   }
-  $short_name = $term['vocabulary']['short_name'];
-  $vocab_desc = $term['vocabulary']['description'];
-  $rows[] = array(
-    array(
-      'data' => 'Name',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $vocab_name,
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Short Name',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $short_name,
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Description',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $vocab_desc,
-  );
 
   $table = array(
     'header' => $headers,
     'rows' => $rows,
     'attributes' => array(),
     'sticky' => FALSE,
-    'caption' => 'Term Vocabulary details',
+    'caption' => '',
     'colgroups' => array(),
     'empty' => '',
   );
-  $content .=  theme_table($table);
-
+  $content['cvterm'] = [
+    '#type' => 'item',
+    '#title' => 'Term Details',
+    '#markup' => theme_table($table),
+  ];
+  
+  $content['vocabulary'] = [
+    '#type' => 'item',
+    '#title' => 'Vocabulary Details',
+    '#markup' => $vocab_table,
+  ];
+  
   drupal_add_js(array(
     'tripal' => array(
       'cv_lookup' => array(

+ 79 - 37
tripal_chado/api/ChadoRecord.inc

@@ -170,11 +170,11 @@ class ChadoRecord {
       foreach ($col_schema as $param => $val) {
         if (preg_match('/not null/i', $param) and $col_schema[$param]) {
           $this->required_cols[] = $column;
+          // Currently all required columns are missing.
+          $this->missing_required_col[$column] = TRUE;
         }
       }
     }
-    // Currently all required columns are missing.
-    $this->missing_required_col = $this->required_cols;
 
     // If a record_id was provided then lookup the record and set the values.
     if ($record_id) {
@@ -189,6 +189,7 @@ class ChadoRecord {
         }
         $this->record_id = $record_id;
         $this->values = $values;
+        $this->missing_required_col = [];
       }
       catch (Exception $e) {
         $message = t('ChadoRecord::_construct(). Could not find a record in table, !table, with the given !pkey: !record_id. ERROR: !error',
@@ -276,8 +277,12 @@ class ChadoRecord {
 
     // Additionally, make sure we have all the required values!
     if (!empty($this->missing_required_col)) {
-      $message = t('ChadoRecord::insert(). The columns named, "!columns", require a value for the table: "!table". You can set these values using ChadoRecord::setValues().',
-        ['!columns' => implode('", "', $this->missing_required_col), '!table' => $this->table_name]);
+      $message = t('ChadoRecord::insert(). The columns named, "!columns", ' . 
+        'require a value for the table: "!table". You can set these values ' .
+        'using ChadoRecord::setValues(). Current values: !values.',
+        ['!columns' => implode('", "', array_keys($this->missing_required_col)), 
+         '!table' => $this->table_name,
+         '!values' => print_r($this->values, TRUE)]);
       throw new Exception($message);
     }
 
@@ -293,18 +298,23 @@ class ChadoRecord {
     $sql = 'INSERT INTO {' . $this->table_name . '} (' .
       implode(", ", $insert_cols) . ') VALUES (' .
       implode(", ", $insert_vals) . ')';
-
+      if ($this->pkey) {
+        $sql .= ' RETURNING ' . $this->pkey;
+      }
     try {
-      chado_query($sql, $insert_args);
-      // @todo we can speed up inserts if we can find a way to not have to
-      // run the find(), but get the newly inserted record_id directly
-      // from the insert command.
-      // One option may be to use the `RETURNING [pkey]` keywords in the SQL statement.
-      $this->find();
+      $result = chado_query($sql, $insert_args);
+      if ($this->pkey) {
+        $record_id = $result->fetchField();
+        $this->values[$this->pkey] = $record_id;
+        $this->record_id = $record_id;
+      }
     }
     catch (Exception $e) {
-      $message = t('ChadoRecord::insert(). Could not insert a record into the table, !table, with the following values: !values. ERROR: !error',
-        ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
+      $message = t('ChadoRecord::insert(). Could not insert a record into the ' . 
+        'table, !table, with the following values: !values. ERROR: !error',
+        ['!table' => $this->table_name, 
+         '!values' => print_r($this->values, TRUE), 
+         '!error' => $e->getMessage()]);
       throw new Exception($message);
     }
   }
@@ -323,22 +333,29 @@ class ChadoRecord {
 
     // Make sure we have values for this record before updating.
     if (empty($this->values)) {
-      $message = t('ChadoRecord::update(). Could not update a record into the table, !table, without any values.',
+      $message = t('ChadoRecord::update(). Could not update a record into the ' . 
+        'table, !table, without any values.',
         ['!table' => $this->table_name]);
       throw new Exception($message);
     }
 
     // Additionally, make sure we have all the required values!
     if (!empty($this->missing_required_col)) {
-      $message = t('ChadoRecord::update(). The columns named, "!columns", require a value for the table: "!table". You can set these values using ChadoRecord::setValues().',
-        ['!columns' => implode('", "', $this->missing_required_col), '!table' => $this->table_name]);
+      $message = t('ChadoRecord::update(). The columns named, "!columns", ' . 
+        'require a value for the table: "!table". You can set these values ' . 
+        'using ChadoRecord::setValues(). Current values: !values.',
+        ['!columns' => implode('", "', $this->missing_required_col), 
+         '!table' => $this->table_name,
+         '!values' => print_r($this->values, TRUE)]);
       throw new Exception($message);
     }
 
     // We have to have a record ID for the record to update.
     if (!$this->record_id) {
-      $message = t('ChadoRecord::update(). Could not update a record in the table, !table, without a record ID.',
-        ['!table' => $this->table_name]);
+      $message = t('ChadoRecord::update(). Could not update a record in the ' .
+        'table, !table, without a record ID. Current values: !values.',
+        ['!table' => $this->table_name,
+         '!values' => print_r($this->values, TRUE)]);
       throw new Exception($message);
     }
 
@@ -363,7 +380,9 @@ class ChadoRecord {
       chado_query($sql, $update_args);
     }
     catch (Exception $e) {
-      $message = t('ChadoRecord::update(). Could not update a record in the table, !table, with !record_id as the record ID and the following values: !values. ERROR: !error',
+      $message = t('ChadoRecord::update(). Could not update a record in the ' .
+        'table, !table, with !record_id as the record ID and the following ' .
+        'values: !values. ERROR: !error',
         ['!table' => $this->table_name,
          '!record_id' => $this->record_id,
          '!values' => print_r($this->values, TRUE),
@@ -431,8 +450,11 @@ class ChadoRecord {
         $this->values[$column] = $value;
       }
       else {
-        $message = t('ChadoRecord::setValues(). The column named, "!column", does not exist in table: "!table". Values: !values".',
-          ['!column' => $column, '!table' => $this->table_name, '!values' => print_r($values, TRUE)]);
+        $message = t('ChadoRecord::setValues(). The column named, "!column", ' . 
+          'does not exist in table: "!table". Values: !values".',
+          ['!column' => $column, 
+           '!table' => $this->table_name, 
+           '!values' => print_r($values, TRUE)]);
         throw new Exception($message);
       }
     }
@@ -448,7 +470,7 @@ class ChadoRecord {
       }
 
       if (in_array($rcol, array_keys($this->values)) and $this->values[$rcol] === '__NULL__') {
-        $this->missing_required_col[$rcol] = $rcol;
+        $this->missing_required_col[$rcol] = TRUE;
       }
     }
 
@@ -460,8 +482,13 @@ class ChadoRecord {
     // Ensure that no values are arrays.
     foreach ($values as $column => $value) {
       if (is_array($value)) {
-        $message = t('ChadoRecord::setValues(). The column named, "!column", must be a single value but is currently: "!values". NOTE: we currently don\'t support expanding foreign key relationships or multiple values for a given column.',
-          ['!column' => $column, '!table' => $this->table_name, '!values' => implode('", "', $value)]);
+        $message = t('ChadoRecord::setValues(). The column named, "!column", ' . 
+          'must be a single value but is currently: "!values". NOTE: this function ' .
+          'currently does not support expanding foreign key relationships or ' .
+          'multiple values for a given column.',
+          ['!column' => $column, 
+           '!table' => $this->table_name, 
+           '!values' => implode('", "', $value)]);
         throw new Exception($message);
       }
     }
@@ -496,27 +523,36 @@ class ChadoRecord {
 
     // Make sure the column is valid.
     if (!in_array($column_name, $this->column_names)) {
-      $message = t('ChadoRecord::setValue(). The column named, "!column", does not exist in table: "!table".',
-        ['!column' => $column_name, '!table' => $this->table_name]);
+      $message = t('ChadoRecord::setValue(). The column named, "!column", does ' . 
+        'not exist in table: "!table".',
+        ['!column' => $column_name, 
+         '!table' => $this->table_name]);
       throw new Exception($message);
     }
 
     // Make sure that the value is not NULL if this is a required field.
     if (!in_array($column_name, $this->required_cols) and $value == '__NULL__') {
-      $message = t('ChadoRecord::setValue(). The column named, "!column", requires a value for the table: "!table".',
-        ['!column' => $column_name, '!table' => $this->table_name]);
+      $message = t('ChadoRecord::setValue(). The column named, "!column", ' . 
+        'requires a value for the table: "!table".',
+        ['!column' => $column_name, 
+         '!table' => $this->table_name]);
       throw new Exception($message);
     }
     
     // Remove from the missing list if it was there.
-    elseif (isset($this->missing_required_cols[$column])) {
-      unset($this->missing_required_cols[$column]);
+    if (isset($this->missing_required_col[$column_name])) {
+      unset($this->missing_required_col[$column_name]);
     }
 
     // Ensure that no values are arrays.
     if (is_array($value)) {
-      $message = t('ChadoRecord::setValue(). The column named, "!column", must be a single value but is currently: "!values". NOTE: we currently don\'t support expanding foreign key relationships or multiple values for a given column.',
-        ['!column' => $column, '!table' => $this->table_name, '!values' => implode('", "', $value)]);
+      $message = t('ChadoRecord::setValue(). The column named, "!column", ' . 
+        'must be a single value but is currently: "!values". NOTE: this function ' . 
+        'currently does not support expanding foreign key relationships or ' .
+        'multiple values for a given column.',
+        ['!column' => $column, 
+         '!table' => $this->table_name, 
+         '!values' => implode('", "', $value)]);
       throw new Exception($message);
     }
 
@@ -533,8 +569,10 @@ class ChadoRecord {
 
     // Make sure the column is valid.
     if (!in_array($column_name, $this->column_names)) {
-      $message = t('ChadoRecord::getValue(). The column named, "!column", does not exist in table: "!table".',
-        ['!column' => $column_name, '!table' => $this->table_name]);
+      $message = t('ChadoRecord::getValue(). The column named, "!column", ' . 
+        'does not exist in table: "!table".',
+        ['!column' => $column_name, 
+         '!table' => $this->table_name]);
       throw new Exception($message);
     }
 
@@ -565,7 +603,8 @@ class ChadoRecord {
 
     // Make sure we have values for this record before searching.
     if (empty($this->values)) {
-      $message = t('ChadoRecord::find(). Could not find a record from the table, !table, without any values.',
+      $message = t('ChadoRecord::find(). Could not find a record from ' . 
+        'the table, !table, without any values.',
         ['!table' => $this->table_name]);
       throw new Exception($message);
     }
@@ -581,8 +620,11 @@ class ChadoRecord {
       $results = chado_query($sql, $select_args);
     }
     catch (Exception $e) {
-      $message = t('ChadoRecord::find(). Could not find a record in the table, !table, with the following values: !values. ERROR: !error',
-        ['!table' => $this->table_name, '!values' => print_r($this->values, TRUE), '!error' => $e->getMessage()]);
+      $message = t('ChadoRecord::find(). Could not find a record in the ' . 
+        'table, !table, with the following values: !values. ERROR: !error',
+        ['!table' => $this->table_name, 
+         '!values' => print_r($this->values, TRUE), 
+         '!error' => $e->getMessage()]);
       throw new Exception($message);
     }
 

+ 629 - 0
tripal_chado/api/ChadoSchema.inc

@@ -0,0 +1,629 @@
+<?php
+/**
+ * Provides an application programming interface (API) for describing Chado tables.
+ *
+ * If you need the Drupal-style array definition for any table, use the following:
+ * @code
+
+    $chado_schema = new \ChadoSchema();
+    $table_schema = $chado_schema->getTableSchema($table_name);
+
+ * @endcode
+ *
+ * where the variable $table contains the name of the table you want to
+ * retireve.  The getTableSchema method determines the appropriate version of
+ * Chado and uses the Drupal hook infrastructure to call the appropriate
+ * hook function to retrieve the table schema.
+ *
+ * Additionally, here are some other examples of how to use this class:
+ * @code
+
+    // Retrieve the schema array for the organism table in chado 1.2
+    $chado_schema = new \ChadoSchema('1.2');
+    $table_schema = $chado_schema->getTableSchema('organism');
+
+    // Retrieve all chado tables.
+    $chado_schema = new \ChadoSchema();
+    $tables = $chado_schema->getTableNames();
+    $base_tables = $chado_schema->getbaseTables();
+
+    // Check the feature.type_id foreign key constraint
+    $chado_schema = new \ChadoSchema();
+    $exists = $chado_schema ->checkFKConstraintExists('feature','type_id');
+
+    // Check Sequence exists
+    $chado_schema = new \ChadoSchema();
+    $exists = $chado_schema->checkSequenceExists('organism','organism_id');
+    // Or just check the primary key directly
+    $compliant = $chado_schema->checkPrimaryKey('organism');
+
+ * @endcode
+ */
+class ChadoSchema {
+
+  /**
+   * @var string
+   *   The current version for this site. E.g. "1.3".
+   */
+  protected $version = '';
+
+  /**
+   * @var string
+   *   The name of the schema chado was installed in.
+   */
+  protected $schema_name = 'chado';
+
+  /**
+   * The ChadoSchema constructor.
+   *
+   * @param string $version
+   *   The current version for this site. E.g. "1.3". If a version is not provided, the
+   *   version of the current database will be looked up.
+   */
+  public function __construct($version = NULL, $schema_name = NULL) {
+
+    // Set the version of the schema.
+    if ($version === NULL) {
+      $this->version = chado_get_version(TRUE);
+    }
+    else {
+      $this->version = $version;
+    }
+
+    // Set the name of the schema.
+    if ($schema_name === NULL) {
+      $this->schema_name = chado_get_schema_name('chado');
+    }
+    else {
+      $this->schema_name = $schema_name;
+    }
+
+    // Check functions require the chado schema be local and installed...
+    // So lets check that now...
+    if (!chado_is_local()) {
+      tripal_report_error(
+        'ChadoSchema',
+        TRIPAL_NOTICE,
+        'The ChadoSchema class requires chado be installed within the drupal database
+          in a separate schema for any compliance checking functionality.'
+      );
+    }
+    if (!chado_is_installed()) {
+      tripal_report_error(
+        'ChadoSchema',
+        TRIPAL_NOTICE,
+        'The ChadoSchema class requires chado be installed
+          for any compliance checking functionality.'
+      );
+    }
+  }
+
+  /**
+   * Returns the version number of the Chado this object references.
+   *
+   * @returns
+   *   The version of Chado
+   */
+  public function getVersion() {
+    return $this->version;
+  }
+
+  /**
+   * Retrieve the name of the PostgreSQL schema housing Chado.
+   *
+   * @return
+   *   The name of the schema.
+   */
+  public function getSchemaName() {
+    return $this->schema_name;
+  }
+
+  /**
+   * Retrieves the list of tables in the Chado schema.  By default it only returns
+   * the default Chado tables, but can return custom tables added to the
+   * Chado schema if requested.
+   *
+   * @param $include_custom
+   *   Optional.  Set as TRUE to include any custom tables created in the
+   *   Chado schema. Custom tables are added to Chado using the
+   *   tripal_chado_chado_create_table() function.
+   *
+   * @returns
+   *   An associative array where the key and value pairs are the Chado table names.
+   */
+  public function getTableNames($include_custom = FALSE) {
+
+    $tables = array();
+    if ($this->version == '1.3') {
+      $tables_v1_3 = tripal_chado_chado_get_v1_3_tables();
+      foreach ($tables_v1_3 as $table) {
+        $tables[$table] = $table;
+      }
+    }
+    if ($this->version == '1.2') {
+      $tables_v1_2 = tripal_chado_chado_get_v1_2_tables();
+      foreach ($tables_v1_2 as $table) {
+        $tables[$table] = $table;
+      }
+    }
+    if ($this->version == '1.11' or $this->version == '1.11 or older') {
+      $tables_v1_11 = tripal_chado_chado_get_v1_11_tables();
+      foreach ($tables_v1_11 as $table) {
+        $tables[$table] = $table;
+      }
+    }
+
+    // now add in the custom tables too if requested
+    if ($include_custom) {
+      $sql = "SELECT table FROM {tripal_custom_tables}";
+      $resource = db_query($sql);
+
+      foreach ($resource as $r) {
+        $tables[$r->table] = $r->table;
+      }
+    }
+
+    asort($tables);
+    return $tables;
+
+  }
+
+  /**
+   * Retrieves the chado tables Schema API array.
+   *
+   * @param $table
+   *   The name of the table to retrieve.  The function will use the appopriate
+   *   Tripal chado schema API hooks (e.g. v1.11 or v1.2).
+   *
+   * @returns
+   *   A Drupal Schema API array defining the table.
+   */
+  public function getTableSchema($table) {
+
+    // first get the chado version.
+    $v = $this->version;
+
+    // get the table array from the proper chado schema
+    $v = preg_replace("/\./", "_", $v); // reformat version for hook name
+
+    // Call the module_invoke_all.
+    $hook_name = "chado_schema_v" . $v . "_" . $table;
+    $table_arr = module_invoke_all($hook_name);
+
+    // If the module_invoke_all returned nothing then let's make sure there isn't
+    // An API call we can call directly.  The only time this occurs is
+    // during an upgrade of a major Drupal version and tripal_core is disabled.
+    if ((!$table_arr or !is_array($table_arr)) and
+          function_exists('tripal_chado_' . $hook_name)) {
+      $api_hook = "tripal_chado_" . $hook_name;
+      $table_arr = $api_hook();
+    }
+
+    // if the table_arr is empty then maybe this is a custom table
+    if (!is_array($table_arr) or count($table_arr) == 0) {
+      $table_arr = $this->getCustomTableSchema($table);
+    }
+
+    return $table_arr;
+
+  }
+
+  /**
+   * Retrieves the schema array for the specified custom table.
+   *
+   * @param $table
+   *   The name of the table to create.
+   *
+   * @return
+   *   A Drupal-style Schema API array definition of the table. Returns
+   *   FALSE on failure.
+   */
+  public function getCustomTableSchema($table) {
+
+    $sql = "SELECT schema FROM {tripal_custom_tables} WHERE table_name = :table_name";
+    $results = db_query($sql, array(':table_name' => $table));
+    $custom = $results->fetchObject();
+    if (!$custom) {
+      return FALSE;
+    }
+    else {
+      return unserialize($custom->schema);
+    }
+  }
+
+  /**
+   *  Returns all chado base tables.
+   *
+   *  Base tables are those that contain the primary record for a data type. For
+   *  example, feature, organism, stock, are all base tables.  Other tables
+   *  include linker tables (which link two or more base tables), property tables,
+   *  and relationship tables.  These provide additional information about
+   *  primary data records and are therefore not base tables.  This function
+   *  retreives only the list of tables that are considered 'base' tables.
+   *
+   *  @return
+   *    An array of base table names.
+   *
+   *  @ingroup tripal_chado_schema_api
+   */
+  function getBaseTables() {
+
+    // Initialize the base tables with those tables that are missing a type.
+    // Ideally they should have a type, but that's for a future version of Chado.
+    $base_tables = array('organism', 'project', 'analysis', 'biomaterial',
+      'eimage', 'assay');
+
+    // We'll use the cvterm table to guide which tables are base tables. Typically
+    // base tables (with a few exceptions) all have a type.  Iterate through the
+    // referring tables.
+    $schema = $this->getTableSchema('cvterm');
+    $referring = $schema['referring_tables'];
+    foreach ($referring as $tablename) {
+
+      // Ignore the cvterm tables, relationships, chadoprop tables.
+      if ($tablename == 'cvterm_dbxref' || $tablename == 'cvterm_relationship' ||
+          $tablename == 'cvtermpath' || $tablename == 'cvtermprop' || $tablename == 'chadoprop' ||
+          $tablename == 'cvtermsynonym' || preg_match('/_relationship$/', $tablename) ||
+          preg_match('/_cvterm$/', $tablename) ||
+          // Ignore prop tables
+          preg_match('/prop$/', $tablename) || preg_match('/prop_.+$/', $tablename) ||
+          // Ignore nd_tables
+          preg_match('/^nd_/', $tablename)) {
+        continue;
+      }
+      else {
+        array_push($base_tables, $tablename);
+      }
+    }
+
+    // Remove any linker tables that have snuck in.  Linker tables are those
+    // whose foreign key constraints link to two or more base table.
+    $final_list = array();
+    foreach ($base_tables as $i => $tablename) {
+      // A few tables break our rule and seems to look
+      // like a linking table, but we want to keep it as a base table.
+      if ($tablename == 'biomaterial' or $tablename == 'assay' or $tablename == 'arraydesign') {
+        $final_list[] = $tablename;
+        continue;
+      }
+
+      // Remove the phenotype table. It really shouldn't be a base table as
+      // it is meant to store individual phenotype measurements.
+      if ($tablename == 'phenotype') {
+        continue;
+      }
+      $num_links = 0;
+      $schema = $this->getTableSchema($tablename);
+      $fkeys = $schema['foreign keys'];
+      foreach ($fkeys as $fkid => $details) {
+        $fktable = $details['table'];
+        if (in_array($fktable, $base_tables)) {
+          $num_links++;
+        }
+      }
+      if ($num_links < 2) {
+        $final_list[] = $tablename;
+      }
+    }
+
+    // Now add in the cvterm table to the list.
+    $final_list[] = 'cvterm';
+
+    // Sort the tables and return the list.
+    sort($final_list);
+    return $final_list;
+
+  }
+
+  /**
+   * Get information about which Chado base table a cvterm is mapped to.
+   *
+   * Vocbulary terms that represent content types in Tripal must be mapped to
+   * Chado tables.  A cvterm can only be mapped to one base table in Chado.
+   * This function will return an object that contains the chado table and
+   * foreign key field to which the cvterm is mapped.  The 'chado_table' property
+   * of the returned object contains the name of the table, and the 'chado_field'
+   * property contains the name of the foreign key field (e.g. type_id), and the
+   * 'cvterm' property contains a cvterm object.
+   *
+   * @params
+   *   An associative array that contains the following keys:
+   *     - cvterm_id:  the cvterm ID value for the term.
+   *     - vocabulary: the short name for the vocabulary (e.g. SO, GO, PATO)
+   *     - accession:  the accession for the term.
+   *     - bundle_id:  the ID for the bundle to which a term is associated.
+   *   The 'vocabulary' and 'accession' must be used together, the 'cvterm_id' can
+   *   be used on it's own.
+   * @return
+   *   An object containing the chado_table and chado_field properties or NULL if
+   *   if no mapping was found for the term.
+   */
+  public function getCvtermMapping($params) {
+    return chado_get_cvterm_mapping($params);
+  }
+
+  /**
+   * Check that any given Chado table exists.
+   *
+   * This function is necessary because Drupal's db_table_exists() function will
+   * not look in any other schema but the one where Drupal is installed
+   *
+   * @param $table
+   *   The name of the chado table whose existence should be checked.
+   *
+   * @return
+   *   TRUE if the table exists in the chado schema and FALSE if it does not.
+   */
+  public function checkTableExists($table) {
+    return chado_table_exists($table);
+  }
+
+  /**
+   * Check that any given column in a Chado table exists.
+   *
+   * This function is necessary because Drupal's db_field_exists() will not
+   * look in any other schema but the one were Drupal is installed
+   *
+   * @param $table
+   *   The name of the chado table.
+   * @param $column
+   *   The name of the column in the chado table.
+   *
+   * @return
+   *   TRUE if the column exists for the table in the chado schema and
+   *   FALSE if it does not.
+   *
+   * @ingroup tripal_chado_schema_api
+   */
+  public function checkColumnExists($table, $column) {
+    return chado_column_exists($table, $column);
+  }
+
+  /**
+   * Check that any given column in a Chado table exists.
+   *
+   * This function is necessary because Drupal's db_field_exists() will not
+   * look in any other schema but the one were Drupal is installed
+   *
+   * @param $table
+   *   The name of the chado table.
+   * @param $column
+   *   The name of the column in the chado table.
+   * @param $type
+   *   (OPTIONAL) The PostgreSQL type to check for. If not supplied it will be
+   *   looked up via the schema (PREFERRED).
+   *
+   * @return
+   *   TRUE if the column type matches what we expect and
+   *   FALSE if it does not.
+   *
+   * @ingroup tripal_chado_schema_api
+   */
+  public function checkColumnType($table, $column, $expected_type = NULL) {
+
+    // Ensure this column exists before moving forward.
+    if (!$this->checkColumnExists($table, $column)) {
+      tripal_report_error(
+        'ChadoSchema',
+        TRIPAL_WARNING,
+        'Unable to check the type of !table!column since it doesn\'t appear to exist in your site database.',
+        array('!column' => $column, '!table' => $table)
+      );
+      return FALSE;
+    }
+
+    // Look up the type using the Schema array.
+    if ($expected_type === NULL) {
+      $schema = $this->getTableSchema($table, $column);
+
+      if (is_array($schema) AND isset($schema['fields'][$column])) {
+        $expected_type = $schema['fields'][$column]['type'];
+      }
+      else {
+        tripal_report_error(
+          'ChadoSchema',
+          TRIPAL_WARNING,
+          'Unable to check the type of !table!column due to being unable to find the schema definition.',
+          array('!column' => $column, '!table' => $table)
+        );
+        return FALSE;
+      }
+    }
+
+    // There is some flexibility in the expected type...
+    // Fix that here.
+    switch ($expected_type) {
+      case 'int':
+        $expected_type = 'integer';
+        break;
+      case 'serial':
+        $expected_type = 'integer';
+        break;
+      case 'varchar':
+        $expected_type = 'character varying';
+        break;
+      case 'datetime':
+        $expected_type = 'timestamp without time zone';
+        break;
+      case 'char':
+        $expected_type = 'character';
+        break;
+    }
+
+    // Grab the type from the current database.
+    $query = 'SELECT data_type
+              FROM information_schema.columns
+              WHERE
+                table_name = :table AND
+                column_name = :column AND
+                table_schema = :schema
+              ORDER  BY ordinal_position
+              LIMIT 1';
+    $type = db_query($query,
+      array(':table' => $table, ':column' => $column, ':schema' => $this->schema_name))->fetchField();
+
+    // Finally we do the check!
+    if ($type === $expected_type) {
+      return TRUE;
+    }
+    elseif (($expected_type == 'float') AND (($type == 'double precision') OR ($type == 'real'))) {
+      return TRUE;
+    }
+    elseif ($type == 'smallint' AND $expected_type == 'integer') {
+      return TRUE;
+    }
+    elseif ($type == 'bigint' AND $expected_type == 'integer') {
+      return TRUE;
+    }
+    else {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Check that any given sequence in a Chado table exists.
+   *
+   * @param table
+   *   The name of the table the sequence is used in.
+   * @param column
+   *   The name of the column the sequence is used to populate.
+   *
+   * @return
+   *   TRUE if the seqeuence exists in the chado schema and FALSE if it does not.
+   *
+   * @ingroup tripal_chado_schema_api
+   */
+  public function checkSequenceExists($table, $column) {
+
+    $prefixed_table = $this->schema_name.'.'.$table;
+    $sequence_name = db_query('SELECT pg_get_serial_sequence(:table, :column);',
+      array(':table' => $prefixed_table, ':column' => $column))->fetchField();
+
+
+    // Remove prefixed table from sequence name
+    $sequence_name = str_replace($this->schema_name.'.', '', $sequence_name);
+
+    return chado_sequence_exists($sequence_name);
+  }
+
+  /**
+   * Check that the primary key exists, has a sequence and a constraint.
+   *
+   * @param $table
+   *   The table you want to check the primary key for.
+   * @param $column
+   *   (OPTIONAL) The name of the primary key column.
+   *
+   * @return
+   *   TRUE if the primary key meets all the requirements and false otherwise.
+   */
+  public function checkPrimaryKey($table, $column = NULL) {
+
+    // If they didn't supply the column, then we can look it up.
+    if ($column === NULL) {
+      $table_schema = $this->getTableSchema($table);
+      $column = $table_schema['primary key'][0];
+    }
+
+    // If there is no primary key then we can't check it.
+    // It neither passes nore fails validation.
+    if (empty($column)) {
+      tripal_report_error(
+        'ChadoSchema',
+        TRIPAL_NOTICE,
+        'Cannot check the validity of the primary key for "!table" since there is no record of one.',
+        array('!table' => $table)
+      );
+      return NULL;
+    }
+
+    // Check the column exists.
+    $column_exists = $this->checkColumnExists($table, $column);
+    if (!$column_exists) {
+      return FALSE;
+    }
+
+    // First check that the sequence exists.
+    $sequence_exists = $this->checkSequenceExists($table, $column);
+    if (!$sequence_exists) {
+      return FALSE;
+    }
+
+    // Next check the constraint is there.
+    $constraint_exists = chado_query(
+      "SELECT 1
+      FROM information_schema.table_constraints
+      WHERE table_name=:table AND constraint_type = 'PRIMARY KEY'",
+      array(':table' => $table))->fetchField();
+    if (!$constraint_exists) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Check that the constraint exists.
+   *
+   * @param $table
+   *   The table the constraint applies to.
+   * @param $constraint_name
+   *   The name of the constraint you want to check.
+   * @param $type
+   *   The type of constraint. Should be one of "PRIMARY KEY", "UNIQUE", or "FOREIGN KEY".
+   *
+   * @return
+   *   TRUE if the constraint exists and false otherwise.
+   */
+  function checkConstraintExists($table, $constraint_name, $type) {
+
+    // Next check the constraint is there.
+    $constraint_exists = chado_query(
+      "SELECT 1
+      FROM information_schema.table_constraints
+      WHERE table_name=:table AND constraint_type = :type AND constraint_name = :name",
+      array(':table' => $table, ':name' => $constraint_name, ':type' => $type))->fetchField();
+    if (!$constraint_exists) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Check the foreign key constrain specified exists.
+   *
+   * @param $base_table
+   *   The name of the table the foreign key resides in. E.g. 'feature' for
+   *     the feature.type_id => cvterm.cvterm_id foreign key.
+   * @param $base_column
+   *   The name of the column that is a foreign key in. E.g. 'type_id' for
+   *     the feature.type_id => cvterm.cvterm_id foreign key.
+   *
+   * @return
+   *   TRUE if the constraint exists and false otherwise.
+   */
+  function checkFKConstraintExists($base_table, $base_column) {
+
+
+    // Since we don't have a constraint name, we have to use the known pattern for
+    // creating these names in order to make this check.
+    // This is due to PostgreSQL not storing column information for constraints
+    // in the information_schema tables.
+    $constraint_name = $base_table . '_' . $base_column . '_fkey';
+
+    return $this->checkConstraintExists($base_table, $constraint_name, 'FOREIGN KEY');
+  }
+
+  /**
+   * A Chado-aware replacement for the db_index_exists() function.
+   *
+   * @param $table
+   *   The table to be altered.
+   * @param $name
+   *   The name of the index.
+   */
+  function checkIndexExists($table, $name) {
+    return chado_index_exists($table, $name);
+  }
+}

Diferenças do arquivo suprimidas por serem muito extensas
+ 270 - 474
tripal_chado/api/modules/tripal_chado.cv.api.inc


+ 15 - 16
tripal_chado/api/modules/tripal_chado.feature.api.inc

@@ -2,7 +2,7 @@
 /**
  * @file
  * Provides API functions specificially for managing feature
- * records in Chado.  
+ * records in Chado.
  */
 
 /**
@@ -10,7 +10,7 @@
  * @ingroup tripal_chado_api
  * @{
  * Provides API functions specificially for managing feature
- * records in Chado especially retrieving relationships and sequences derived 
+ * records in Chado especially retrieving relationships and sequences derived
  * from relationships and feature alignments.
  * @}
  */
@@ -95,8 +95,8 @@ function chado_reverse_compliment_sequence($sequence) {
  *    - name: The feature name. This will appear on the FASTA definition line.
  *    - parent_id:  (optional) only retrieve a sequence if 'derive_from_parent'
  *      is true and the parent matches this ID.
- *    - featureloc_id: (optional) only retrieve a sequence if 
- *      'derive_from_parent' is true and the alignment is defined with this 
+ *    - featureloc_id: (optional) only retrieve a sequence if
+ *      'derive_from_parent' is true and the alignment is defined with this
  *      featureloc_id.
  * @param $options
  *   An associative array of options. Valid keys include:
@@ -305,7 +305,7 @@ function chado_get_feature_sequences($feature, $options) {
         INNER JOIN {feature} OF   on FL.srcfeature_id = OF.feature_id
         INNER JOIN {cvterm}  OCVT on OF.type_id       = OCVT.cvterm_id
         INNER JOIN {organism} OO  on OF.organism_id   = OO.organism_id
-      WHERE SF.feature_id = :feature_id and NOT (OF.residues = \'\' or OF.residues IS NULL)) as tbl1
+      WHERE SF.feature_id = :feature_id and NOT (OF.residues = \'\' or OF.residues IS NULL) ORDER BY fmin) as tbl1
   ';
   // This query is meant to get all of the sub features of any given
   // feature (arg #1) and order them as they appear on the reference
@@ -392,8 +392,8 @@ function chado_get_feature_sequences($feature, $options) {
             $types[] = $child->type_name;
           }
 
-          // If the first sub feature we need to include the upstream bases. 
-          // First check if the feature is in the foward direction or the 
+          // If the first sub feature we need to include the upstream bases.
+          // First check if the feature is in the foward direction or the
           // reverse.
           if ($i == 0 and $parent->strand >= 0) {  // forward direction
             // -------------------------- ref
@@ -408,8 +408,8 @@ function chado_get_feature_sequences($feature, $options) {
             $q = chado_query($parent_sql, array(':upstream' => 0, ':downstream' => $downstream, ':feature_id' => $child->feature_id));
           }
 
-          // Next, if the last sub feature we need to include the downstream 
-          // bases. First check if the feature is in teh forward direction or 
+          // Next, if the last sub feature we need to include the downstream
+          // bases. First check if the feature is in teh forward direction or
           // the reverse.
           elseif ($i == $num_children - 1 and $parent->strand >= 0) {  // forward direction
             // -------------------------- ref
@@ -697,15 +697,15 @@ function chado_get_bulk_feature_sequences($options) {
  * Returns a definition line that can be used in a FASTA file.
  *
  * @param $feature
- *   A single feature object containing all the fields from the chado.feature 
- *   table. Best case is to provide an object generated by the 
+ *   A single feature object containing all the fields from the chado.feature
+ *   table. Best case is to provide an object generated by the
  *   chado_generate_var() function.
  * @param $notes
  *   Optional: additional notes to be added to the definition line.
  * @param $featureloc
  *   Optional: a single featureloc object generated using chado_generate_var
  *   that contains a record from the chado.featureloc table. Provide this if the
- *   sequence was obtained by using the alignment rather than from the 
+ *   sequence was obtained by using the alignment rather than from the
  *   feature.residues column.
  * @param $type
  *   Optional: the type of sequence. By default the feature type is used.
@@ -714,9 +714,9 @@ function chado_get_bulk_feature_sequences($options) {
  *
  * @return
  *   A string of the format: uniquename|name|type|feature_id
- *   or if an alignment:  srcfeature_name:fmin..fmax[+-]; alignment of 
+ *   or if an alignment:  srcfeature_name:fmin..fmax[+-]; alignment of
  *   uniquename|name|type|feature_id.
- * 
+ *
  * @ingroup tripal_feature_api
  */
 function chado_get_fasta_defline($feature, $notes = '', $featureloc = NULL, $type = '', $length = 0) {
@@ -769,7 +769,7 @@ function chado_get_fasta_defline($feature, $notes = '', $featureloc = NULL, $typ
  *
  *  @return
  *   A string of the format: uniquename:featurelocmin..featurelocmax.strand
- * 
+ *
  * @ingroup tripal_feature_api
  */
 function chado_get_location_string($featureloc) {
@@ -785,4 +785,3 @@ function chado_get_location_string($featureloc) {
 
   return $featureloc->srcfeature_id->name . ":" . ($featureloc->fmin + 1) . ".." . $featureloc->fmax .  $strand;
 }
-

+ 1 - 3
tripal_chado/api/modules/tripal_chado.module.DEPRECATED.api.inc

@@ -209,9 +209,7 @@ function tripal_get_cvterm_select_options($cv_id, $rel_type = false) {
 /**
  * Duplicate of fill_cvtermpath() stored procedure in Chado.
  *
- * Identifies all of the root terms of the controlled vocabulary. These
- * root terms are then processed by calling the
- * chado_update_cvtermpath_root_loop() function on each one.
+ * Identifies all of the root terms of the controlled vocabulary. 
  *
  * @param $cvid
  *   The controlled vocabulary ID from the cv table of Chado (i.e. cv.cv_id).

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

@@ -386,7 +386,7 @@ function chado_delete_mview($mview_id) {
 }
 
 /**
- * Update a Materialized View.
+ * Populate a Materialized View.
  *
  * @param $mview_id
  *   The unique identifier for the materialized view to be updated.

+ 27 - 3
tripal_chado/api/tripal_chado.schema_v1.2.api.inc

@@ -2820,6 +2820,7 @@ function tripal_chado_chado_schema_v1_2_control() {
     'primary key' => array(
       0 => 'control_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'control_idx1' => array(
         0 => 'type_id',
@@ -3858,6 +3859,7 @@ function tripal_chado_chado_schema_v1_2_eimage() {
     'primary key' => array(
       0 => 'eimage_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
     ),
     'table' => 'eimage',
@@ -6559,6 +6561,7 @@ function tripal_chado_chado_schema_v1_2_featuremap_pub() {
     'primary key' => array(
       0 => 'featuremap_pub_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'featuremap_pub_idx1' => array(
         0 => 'featuremap_id',
@@ -6633,6 +6636,7 @@ function tripal_chado_chado_schema_v1_2_featurepos() {
     'primary key' => array(
       0 => 'featurepos_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'featurepos_idx1' => array(
         0 => 'featuremap_id',
@@ -6878,6 +6882,7 @@ function tripal_chado_chado_schema_v1_2_featurerange() {
     'primary key' => array(
       0 => 'featurerange_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'featurerange_idx1' => array(
         0 => 'featuremap_id',
@@ -7789,6 +7794,7 @@ function tripal_chado_chado_schema_v1_2_magedocumentation() {
     'primary key' => array(
       0 => 'magedocumentation_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'magedocumentation_idx1' => array(
         0 => 'mageml_id',
@@ -7855,6 +7861,7 @@ function tripal_chado_chado_schema_v1_2_mageml() {
     'primary key' => array(
       0 => 'mageml_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
     ),
     'table' => 'mageml',
@@ -7900,6 +7907,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment() {
     'primary key' => array(
       0 => 'nd_experiment_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'cvterm' => array(
         'table' => 'cvterm',
@@ -7965,6 +7973,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment_contact() {
     'primary key' => array(
       0 => 'nd_experiment_contact_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'contact' => array(
         'table' => 'contact',
@@ -8020,6 +8029,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment_dbxref() {
     'primary key' => array(
       0 => 'nd_experiment_dbxref_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'dbxref' => array(
         'table' => 'dbxref',
@@ -8197,6 +8207,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment_project() {
     'primary key' => array(
       0 => 'nd_experiment_project_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'project' => array(
         'table' => 'project',
@@ -8252,6 +8263,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment_protocol() {
     'primary key' => array(
       0 => 'nd_experiment_protocol_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'nd_experiment' => array(
         'table' => 'nd_experiment',
@@ -8381,6 +8393,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment_stock() {
     'primary key' => array(
       0 => 'nd_experiment_stock_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'cvterm' => array(
         'table' => 'cvterm',
@@ -8445,6 +8458,7 @@ function tripal_chado_chado_schema_v1_2_nd_experiment_stock_dbxref() {
     'primary key' => array(
       0 => 'nd_experiment_stock_dbxref_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'dbxref' => array(
         'table' => 'dbxref',
@@ -8663,8 +8677,8 @@ function tripal_chado_chado_schema_v1_2_nd_geolocation() {
     'primary key' => array(
       0 => 'nd_geolocation_id',
     ),
-    'foreign keys' => array(
-    ),
+    'unique keys' => array(),
+    'foreign keys' => array(),
     'table' => 'nd_geolocation',
     'referring_tables' => array(
       0 => 'nd_experiment',
@@ -8846,6 +8860,7 @@ function tripal_chado_chado_schema_v1_2_nd_protocol_reagent() {
     'primary key' => array(
       0 => 'nd_protocol_reagent_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'cvterm' => array(
         'table' => 'cvterm',
@@ -8986,6 +9001,7 @@ function tripal_chado_chado_schema_v1_2_nd_reagent() {
     'primary key' => array(
       0 => 'nd_reagent_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'cvterm' => array(
         'table' => 'cvterm',
@@ -9044,6 +9060,7 @@ function tripal_chado_chado_schema_v1_2_nd_reagent_relationship() {
     'primary key' => array(
       0 => 'nd_reagent_relationship_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'cvterm' => array(
         'table' => 'cvterm',
@@ -10513,6 +10530,7 @@ function tripal_chado_chado_schema_v1_2_phylotree() {
     'primary key' => array(
       0 => 'phylotree_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'phylotree_idx1' => array(
         0 => 'phylotree_id',
@@ -11123,6 +11141,7 @@ function tripal_chado_chado_schema_v1_2_protocolparam() {
     'primary key' => array(
       0 => 'protocolparam_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'protocolparam_idx1' => array(
         0 => 'protocol_id',
@@ -12645,6 +12664,7 @@ function tripal_chado_chado_schema_v1_2_stock_relationship_cvterm() {
     'primary key' => array(
       0 => 'stock_relationship_cvterm_id',
     ),
+    'unique keys' => array(),
     'foreign keys' => array(
       'cvterm' => array(
         'table' => 'cvterm',
@@ -13331,6 +13351,7 @@ function tripal_chado_chado_schema_v1_2_studydesign() {
     'primary key' => array(
       0 => 'studydesign_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'studydesign_idx1' => array(
         0 => 'study_id',
@@ -13479,6 +13500,7 @@ function tripal_chado_chado_schema_v1_2_studyfactor() {
     'primary key' => array(
       0 => 'studyfactor_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'studyfactor_idx1' => array(
         0 => 'studydesign_id',
@@ -13560,6 +13582,7 @@ function tripal_chado_chado_schema_v1_2_studyfactorvalue() {
     'primary key' => array(
       0 => 'studyfactorvalue_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'studyfactorvalue_idx1' => array(
         0 => 'studyfactor_id',
@@ -13954,6 +13977,7 @@ function tripal_chado_chado_schema_v1_2_treatment() {
     'primary key' => array(
       0 => 'treatment_id',
     ),
+    'unique keys' => array(),
     'indexes' => array(
       'treatment_idx1' => array(
         0 => 'biomaterial_id',
@@ -14179,4 +14203,4 @@ function tripal_chado_chado_get_v1_2_tables() {
     'treatment'
    );
    return $tables;
-}
+}

+ 19 - 17
tripal_chado/api/tripal_chado.schema_v1.3.api.inc

@@ -4118,7 +4118,7 @@ function tripal_chado_chado_schema_v1_3_cvtermprop() {
       ),
     ),
     'unique keys' => array(
-      'cvterm_id_type_id_value_rank' => array(
+      'cvtermprop_cvterm_id_type_id_value_rank_key' => array(
         0 => 'cvterm_id',
         1 => 'type_id',
         2 => 'value',
@@ -7019,13 +7019,14 @@ function tripal_chado_chado_schema_v1_3_featuremap_dbxref() {
         'default' => 'true',
       ),
     ),
-    // TODO: this unique constraint is missing from the actual Chado schema.
-    // It should be included.
     'unique keys' => array(
+      /* @todo: this unique constraint is missing from the actual Chado schema.
+          It should be included.
       'feature_dbxref_c1' => array(
         0 => 'featuremap_id',
         1 => 'dbxref_id',
       ),
+      */
     ),
     'indexes' => array(
       'featuremap_dbxref_idx1' => array(
@@ -7252,13 +7253,14 @@ function tripal_chado_chado_schema_v1_3_featuremap_pub() {
         'not null' => TRUE,
       ),
     ),
-    // TODO: this unique constraint is missing from the actual Chado schema.
-    // It should be included.
     'unique keys' => array(
+      /* @todo: this unique constraint is missing from the actual Chado schema.
+          It should be included.
       'feature_pub_c1' => array(
         0 => 'featuremap_id',
         1 => 'pub_id',
       ),
+      */
     ),
     'indexes' => array(
       'featuremap_pub_idx1' => array(
@@ -9936,7 +9938,7 @@ function tripal_chado_chado_schema_v1_3_materialized_view() {
       ),
     ),
     'unique keys' => array(
-      'name' => array(
+      'materialized_view_name_key' => array(
         0 => 'name',
       ),
     ),
@@ -11123,7 +11125,7 @@ function tripal_chado_chado_schema_v1_3_nd_protocol() {
       ),
     ),
     'unique keys' => array(
-      'name' => array(
+      'nd_protocol_name_key' => array(
         0 => 'name',
       ),
     ),
@@ -13094,11 +13096,11 @@ function tripal_chado_chado_schema_v1_3_phylonode() {
       ),
     ),
     'unique keys' => array(
-      'phylotree_id_left_idx' => array(
+      'phylonode_phylotree_id_left_idx_key' => array(
         0 => 'phylotree_id',
         1 => 'left_idx',
       ),
-      'phylotree_id_right_idx' => array(
+      'phylonode_phylotree_id_right_idx_key' => array(
         0 => 'phylotree_id',
         1 => 'right_idx',
       ),
@@ -13189,7 +13191,7 @@ function tripal_chado_chado_schema_v1_3_phylonode_dbxref() {
       ),
     ),
     'unique keys' => array(
-      'phylonode_id_dbxref_id' => array(
+      'phylonode_dbxref_phylonode_id_dbxref_id_key' => array(
         0 => 'phylonode_id',
         1 => 'dbxref_id',
       ),
@@ -13262,7 +13264,7 @@ function tripal_chado_chado_schema_v1_3_phylonode_organism() {
       ),
     ),
     'unique keys' => array(
-      'phylonode_id' => array(
+      'phylonode_organism_phylonode_id_key' => array(
         0 => 'phylonode_id',
       ),
     ),
@@ -13346,7 +13348,7 @@ function tripal_chado_chado_schema_v1_3_phylonodeprop() {
       ),
     ),
     'unique keys' => array(
-      'phylonode_id_type_id_value_rank' => array(
+      'phylonodeprop_phylonode_id_type_id_value_rank_key' => array(
         0 => 'phylonode_id',
         1 => 'type_id',
         2 => 'value',
@@ -13421,7 +13423,7 @@ function tripal_chado_chado_schema_v1_3_phylonode_pub() {
       ),
     ),
     'unique keys' => array(
-      'phylonode_id_pub_id' => array(
+      'phylonode_pub_phylonode_id_pub_id_key' => array(
         0 => 'phylonode_id',
         1 => 'pub_id',
       ),
@@ -13515,7 +13517,7 @@ function tripal_chado_chado_schema_v1_3_phylonode_relationship() {
       0 => 'phylonode_relationship_id',
     ),
     'unique keys' => array(
-      'subject_id_object_id_type_id' => array(
+      'phylonode_relationship_subject_id_object_id_type_id_key' => array(
         0 => 'subject_id',
         1 => 'object_id',
         2 => 'type_id',
@@ -13768,7 +13770,7 @@ function tripal_chado_chado_schema_v1_3_phylotree_pub() {
       ),
     ),
     'unique keys' => array(
-      'phylotree_id_pub_id' => array(
+      'phylotree_pub_phylotree_id_pub_id_key' => array(
         0 => 'phylotree_id',
         1 => 'pub_id',
       ),
@@ -17676,7 +17678,7 @@ function tripal_chado_chado_schema_v1_3_studyprop() {
       0 => 'studyprop_id',
     ),
     'unique keys' => array(
-      'study_id_type_id_rank' => array(
+      'studyprop_study_id_type_id_rank_key' => array(
         0 => 'study_id',
         1 => 'type_id',
         2 => 'rank',
@@ -17756,7 +17758,7 @@ function tripal_chado_chado_schema_v1_3_studyprop_feature() {
       0 => 'studyprop_feature_id',
     ),
     'unique keys' => array(
-      'studyprop_id_feature_id' => array(
+      'studyprop_feature_studyprop_id_feature_id_key' => array(
         0 => 'studyprop_id',
         1 => 'feature_id',
       ),

+ 1 - 0
tripal_chado/api/tripal_chado.variables.api.inc

@@ -314,6 +314,7 @@ function chado_generate_var($table, $values, $base_options = array()) {
       // check if the current table maps to a node type-------------------------
       // If this table is connected to a node there will be a chado_tablename 
       // table in drupal.
+      $base_tables = chado_get_base_tables();
       if (module_exists('tripal_core') and db_table_exists('chado_' . $table)) {
         // That has a foreign key to this one ($table_desc['primary key'][0]
         // and to the node table (nid).

+ 53 - 52
tripal_chado/files/tcontact.obo

@@ -1,150 +1,151 @@
 format-version: 1.2
 default-namespace: tripal_contact
+ontology: tcontact
 
 [Term]
-id: TContact:0000001
+id: TCONTACT:0000001
 name: Contact Type
 
 
 [Term]
-id: TContact:0000002
+id: TCONTACT:0000002
 name: Collective
 def: Used when a contact is a collective of individuals rather than a person.
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000003
+id: TCONTACT:0000003
 name: Person
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000004
+id: TCONTACT:0000004
 name: Organization
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000005
+id: TCONTACT:0000005
 name: University
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000006
+id: TCONTACT:0000006
 name: Lab
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000007
+id: TCONTACT:0000007
 name: Institute
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000008
+id: TCONTACT:0000008
 name: Research Group
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000009
+id: TCONTACT:0000009
 name: Department
-is_a: TContact:0000001 ! Contact Type
+is_a: TCONTACT:0000001 ! Contact Type
 
 [Term]
-id: TContact:0000010
+id: TCONTACT:0000010
 name: First Initials
 def: The first initials for the author including the initial for the first name and any middle names (not the initial for the last name).
-relationship: part_of TContact:0000003 ! Person
+relationship: part_of TCONTACT:0000003 ! Person
 
 [Term]
-id: TContact:0000011
+id: TCONTACT:0000011
 name: Surname
 synonym: "family_name" EXACT []
 synonym: "last_name" EXACT []
-relationship: part_of TContact:0000003 ! Person
+relationship: part_of TCONTACT:0000003 ! Person
 
 [Term]
-id: TContact:0000012
+id: TCONTACT:0000012
 name: Given Name
 synonym: "first_name" EXACT []
-relationship: part_of TContact:0000003 ! Person
+relationship: part_of TCONTACT:0000003 ! Person
 
 [Term]
-id: TContact:0000013
+id: TCONTACT:0000013
 name: Middle Names
 def: One or more middle names for this person.
-relationship: part_of TContact:0000003 ! Person
+relationship: part_of TCONTACT:0000003 ! Person
 
 [Term]
-id: TContact:0000014
+id: TCONTACT:0000014
 name: Middle Initials
 def: The middle initials for this person excluding the initial for the given name and the surname.
-relationship: part_of TContact:0000003 ! Person
+relationship: part_of TCONTACT:0000003 ! Person
 
 [Term]
-id: TContact:0000015
+id: TCONTACT:0000015
 name: Affiliation
 
 [Term]
-id: TContact:0000016
+id: TCONTACT:0000016
 name: Department
 def: The department of an institution or organization.
-relationship: part_of TContact:0000015 ! Affiliation
+relationship: part_of TCONTACT:0000015 ! Affiliation
 
 [Term]
-id: TContact:0000017
+id: TCONTACT:0000017
 name: Institution
-relationship: part_of TContact:0000015 ! Affiliation
+relationship: part_of TCONTACT:0000015 ! Affiliation
 
 [Term]
-id: TContact:0000018
+id: TCONTACT:0000018
 name: Organization
 def: A generic term for any organization.
-relationship: part_of TContact:0000015 ! Affiliation
+relationship: part_of TCONTACT:0000015 ! Affiliation
 
 [Term]
-id: TContact:0000019
+id: TCONTACT:0000019
 name: Address
 
 [Term]
-id: TContact:0000020
+id: TCONTACT:0000020
 name: Address Line 1
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000021
+id: TCONTACT:0000021
 name: Address Line 2
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000022
+id: TCONTACT:0000022
 name: Address Line 3
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000023
+id: TCONTACT:0000023
 name: City
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000024
+id: TCONTACT:0000024
 name: State
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000025
+id: TCONTACT:0000025
 name: Province
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000026
+id: TCONTACT:0000026
 name: Country
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000027
+id: TCONTACT:0000027
 name: Postal Code
-relationship: part_of TContact:0000019 ! Address
+relationship: part_of TCONTACT:0000019 ! Address
 
 [Term]
-id: TContact:0000028
+id: TCONTACT:0000028
 name: contact_description
 def: A description of the contact
 

+ 1 - 2
tripal_chado/files/tpub.obo

@@ -1,6 +1,7 @@
 format-version: 1.2
 default-namespace: tripal_pub
 subsetdef: MeSH_Publication_Type "MeSH Publication Types"
+ontology: tpub
 
 [Term]
 id: TPUB:0000001
@@ -742,8 +743,6 @@ def: Works consisting of or containing a substantial number of blank forms.
 is_a: TPUB:0000015 ! Publication Type
 subset: MeSH_Publication_Type
 
-subset: MeSH_Publication_Type
-
 [Term]
 id: TPUB:0000157
 name: Formularies

Diferenças do arquivo suprimidas por serem muito extensas
+ 1306 - 689
tripal_chado/includes/TripalImporter/OBOImporter.inc


+ 16 - 15
tripal_chado/includes/setup/tripal_chado.chado_vx_x.inc

@@ -452,14 +452,15 @@ function tripal_chado_add_db2cv_mview_mview() {
   );
 
   $sql = "
-   SELECT DISTINCT CV.cv_id, CV.name as cvname, DB.db_id, DB.name as dbname,
-     COUNT(CVT.cvterm_id) as num_terms
-   FROM cv CV
-     INNER JOIN cvterm CVT on CVT.cv_id = CV.cv_id
-     INNER JOIN dbxref DBX on DBX.dbxref_id = CVT.dbxref_id
-     INNER JOIN db DB on DB.db_id = DBX.db_id
-   GROUP BY CV.cv_id, CV.name, DB.db_id, DB.name
-   ORDER BY DB.name
+    SELECT DISTINCT CV.cv_id, CV.name as cvname, DB.db_id, DB.name as dbname,
+      COUNT(CVT.cvterm_id) as num_terms
+    FROM cv CV
+      INNER JOIN cvterm CVT on CVT.cv_id = CV.cv_id
+      INNER JOIN dbxref DBX on DBX.dbxref_id = CVT.dbxref_id
+      INNER JOIN db DB on DB.db_id = DBX.db_id
+    WHERE CVT.is_relationshiptype = 0 and CVT.is_obsolete = 0
+    GROUP BY CV.cv_id, CV.name, DB.db_id, DB.name
+    ORDER BY DB.name
   ";
 
   // Create the MView
@@ -506,13 +507,13 @@ function tripal_chado_add_cv_root_mview_mview() {
   );
 
   $sql = "
-    SELECT DISTINCT CVT.name,CVT.cvterm_id, CV.cv_id, CV.name
-    FROM cvterm_relationship CVTR
-      INNER JOIN cvterm CVT on CVTR.object_id = CVT.cvterm_id
-      INNER JOIN cv CV on CV.cv_id = CVT.cv_id
-    WHERE CVTR.object_id not in
-      (SELECT subject_id FROM cvterm_relationship)
-    AND CVT.is_relationshiptype = 0
+    SELECT DISTINCT CVT.name, CVT.cvterm_id, CV.cv_id, CV.name
+    FROM cvterm CVT
+      LEFT JOIN cvterm_relationship CVTR ON CVT.cvterm_id = CVTR.subject_id
+      INNER JOIN cvterm_relationship CVTR2 ON CVT.cvterm_id = CVTR2.object_id
+    INNER JOIN cv CV on CV.cv_id = CVT.cv_id
+    WHERE CVTR.subject_id is NULL and 
+      CVT.is_relationshiptype = 0 and CVT.is_obsolete = 0
   ";
 
   // Create the MView

+ 39 - 0
tripal_chado/includes/setup/tripal_chado.setup.inc

@@ -71,6 +71,45 @@ function tripal_chado_prepare_drush_submit() {
  *
  */
 function tripal_chado_load_ontologies() {
+  
+  // Before we can load ontologies we need a few terms that unfortunately 
+  // don't get added until later. We'll add them now so the loader works.
+  chado_insert_db([
+    'name' => 'NCIT',
+    'description' => 'NCI Thesaurus OBO Edition.',
+    'url' => 'http://purl.obolibrary.org/obo/ncit.owl',
+    'urlprefix' => ' http://purl.obolibrary.org/obo/{db}_{accession}',
+  ]);
+  chado_insert_cv(
+    'ncit',
+    'The NCIt OBO Edition project aims to increase integration of the NCIt with OBO Library ontologies. NCIt is a reference terminology that includes broad coverage of the cancer domain, including cancer related diseases, findings and abnormalities. NCIt OBO Edition releases should be considered experimental.'
+    );
+  
+  $term = chado_insert_cvterm([
+    'id' => 'NCIT:C25693',
+    'name' => 'Subgroup',
+    'cv_name' => 'ncit',
+    'definition' => 'A subdivision of a larger group with members often exhibiting similar characteristics. [ NCI ]',
+  ]);
+  
+  
+  // Add the rdfs:comment vocabulary.
+  chado_insert_db(array(
+    'name' => 'rdfs',
+    'description' => 'Resource Description Framework Schema',
+    'url' => 'https://www.w3.org/TR/rdf-schema/',
+    'urlprefix' => 'http://www.w3.org/2000/01/rdf-schema#{accession}',
+  ));
+  chado_insert_cv(
+    'rdfs',
+    'Resource Description Framework Schema'
+    );
+  $name = chado_insert_cvterm(array(
+    'id' => 'rdfs:comment',
+    'name' => 'comment',
+    'cv_name' => 'rdfs',
+    'definition' => 'A human-readable description of a resource\'s name.',
+  ));
 
   // Insert commonly used ontologies into the tables.
   $ontologies = array(

+ 4 - 1
tripal_chado/includes/tripal_chado.bundle.inc

@@ -64,7 +64,7 @@ function tripal_chado_bundle_create($bundle, $storage_args) {
       throw new Exception('Cannot create content type. Problem associating type with Chado.');
     }
   }
-
+  
   tripal_chado_create_bundle_table($bundle);
 }
 
@@ -114,6 +114,9 @@ function tripal_chado_create_bundle_table($bundle) {
     ),
   );
   $chado_entity_table = chado_get_bundle_entity_table($bundle);
+  if (!$chado_entity_table) {
+    throw new Exception('Cannot create entity linker table for chado.');
+  }
   db_create_table($chado_entity_table, $schema);
 }
 

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

@@ -142,9 +142,13 @@ function tripal_chado_entity_update($entity, $type) {
  * Implements hook_entity_delete().
  */
 function tripal_chado_entity_delete($entity, $type) {
-  if ($type == 'TripalEntity') {
-
+  if ($type !== 'TripalEntity') {
+    return;
   }
+
+  // Delete the removed entity from the corresponding chado_bio_data_x table
+  $bundle = $entity->bundle;
+  db_delete("chado_{$bundle}")->condition('entity_id', $entity->id)->execute();
 }
 
 /**

+ 1 - 1
tripal_chado/includes/tripal_chado.fields.inc

@@ -1313,7 +1313,7 @@ function tripal_chado_bundle_instances_info_custom(&$info, $entity_type, $bundle
         break;
       case 'contact':
         $default_vocab = 'tripal_contact';
-        $parent_term = 'TContact:0000001';
+        $parent_term = 'TCONTACT:0000001';
         $description = 'Select the type.';
         break;
       default:

+ 2 - 7
tripal_chado/includes/tripal_chado.install.inc

@@ -85,7 +85,7 @@ function tripal_chado_load_form($form, $form_state) {
   );
 
   $form['action_to_do'] = array(
-    '#type' => 'radios',
+    '#type' => 'select',
     '#title' => 'Installation/Upgrade Action',
     '#options' => array(
       'Install Chado v1.3' => t('New Install of Chado v1.3 (erases all existing Chado data if Chado already exists)'),
@@ -94,7 +94,7 @@ function tripal_chado_load_form($form, $form_state) {
       'Upgrade Chado v1.11 to v1.2' => t('Upgrade existing Chado v1.11 to v1.2 (no data is lost)'),
       'Install Chado v1.11' => t('New Install of Chado v1.11 (erases all existing Chado data if Chado already exists)'),
     ),
-    '#description' => t('Select an action to perform. If you want to install Chado all other Tripal modules must not be installed.'),
+    '#description' => t('Select an action to perform.'),
     '#required' => TRUE,
     '#ajax' => array(
       'callback' => "tripal_chado_load_form_ajax_callback",
@@ -104,11 +104,6 @@ function tripal_chado_load_form($form, $form_state) {
     ),
   );
 
-  $form['warning'] = array(
-    '#markup' => "<div><font color=\"red\">WARNING:</font>" . t('A new install of
-      Chado will remove and recreate the Chado database if it already exists.') . '</div>',
-  );
-
   $form['button'] = array(
     '#type' => 'submit',
     '#value' => t('Install/Upgrade Chado'),

+ 18 - 4
tripal_chado/includes/tripal_chado.semweb.inc

@@ -157,6 +157,12 @@ function tripal_chado_populate_vocab_RDFS() {
     'cv_name' => 'rdfs',
     'definition' => 'A human-readable version of a resource\'s name.',
   ));
+  $name = chado_insert_cvterm(array(
+    'id' => 'rdfs:comment',
+    'name' => 'comment',
+    'cv_name' => 'rdfs',
+    'definition' => 'A human-readable description of a resource\'s name.',
+  ));
 }
 /**
  * Adds the Schema.org database and terms.
@@ -1831,10 +1837,10 @@ function tripal_chado_populate_vocab_SWO() {
  */
 function tripal_chado_populate_vocab_TCONTACT() {
   chado_insert_db(array(
-    'name' => 'TContact',
+    'name' => 'TCONTACT',
     'description' => 'Tripal Contact Ontology. A temporary ontology until a more formal appropriate ontology an be identified.',
-    'url' => 'cv/lookup/TContact',
-    'urlprefix' => 'cv/lookup/TContact/{accession}',
+    'url' => 'cv/lookup/TCONTACT',
+    'urlprefix' => 'cv/lookup/TCONTACT/{accession}',
   ));
   chado_insert_cv('tripal_contact', 'Tripal Contact Ontology. A temporary ontology until a more formal appropriate ontology an be identified.');
 }
@@ -1947,7 +1953,7 @@ function tripal_chado_populate_vocab_TAXRANK() {
  */
 function tripal_chado_populate_vocab_NCIT() {
   chado_insert_db(array(
-    'name' => 'NCI Thesaurus OBO Edition',
+    'name' => 'NCIT',
     'description' => 'NCI Thesaurus OBO Edition.',
     'url' => 'http://purl.obolibrary.org/obo/ncit.owl',
     'urlprefix' => ' http://purl.obolibrary.org/obo/{db}_{accession}',
@@ -2111,6 +2117,14 @@ function tripal_chado_populate_vocab_NCIT() {
     'cv_name' => 'ncit',
     'definition' => 'Any genetically determined characteristic.',
   ));
+  
+  $term = chado_insert_cvterm(array(
+    'id' => 'NCIT:C25693',
+    'name' => 'Subgroup',
+    'cv_name' => 'ncit',
+    'definition' => 'A subdivision of a larger group with members often exhibiting similar characteristics. [ NCI ]',
+  ));
+  
 }
 
 /**

+ 56 - 6
tripal_chado/includes/tripal_chado.vocab_storage.inc

@@ -201,6 +201,7 @@ function tripal_chado_vocab_get_terms($vocabulary, $limit = 25, $element = 0) {
   }
   return $terms;
 }
+
 /**
  * Implements hook_vocab_get_term_children().
  *
@@ -235,12 +236,15 @@ function tripal_chado_vocab_get_term_children($vocabulary, $accession) {
 
   // Get the children.
   $sql = "
-    SELECT DISTINCT subject_id
+    SELECT DISTINCT CVTR.subject_id, CVT.name
     FROM {cvterm_relationship} CVTR
+      INNER JOIN {cvterm} CVT on CVTR.subject_id = CVT.cvterm_id
     WHERE object_id = :object_id
+    ORDER BY CVT.name
   ";
   $results = chado_query($sql, array(':object_id' => $cvterm->cvterm_id));
-  while($cvterm_id = $results->fetchField()) {
+  while($cvterm = $results->fetchObject()) {
+    $cvterm_id = $cvterm->subject_id;
     $match = array('cvterm_id' => $cvterm_id);
     $cvterm = chado_generate_var('cvterm', $match);
     $terms[] = _tripal_chado_format_term_description($cvterm);
@@ -274,7 +278,12 @@ function tripal_chado_vocab_get_term($vocabulary, $accession) {
   if (!$cvterm) {
     return FALSE;
   }
-  $cvterm = chado_expand_var($cvterm, 'field', 'cvterm.definition');
+  $options = ['return_array' => TRUE];
+  $cvterm = chado_expand_var($cvterm, 'field', 'cvterm.definition', $options);
+  $cvterm = chado_expand_var($cvterm, 'table', 'cvterm_dbxref', $options);
+  $cvterm = chado_expand_var($cvterm, 'table', 'cvtermsynonym', $options);
+  $cvterm = chado_expand_var($cvterm, 'table', 'cvterm_relationship', $options);
+  $cvterm = chado_expand_var($cvterm, 'table', 'cvtermprop', $options);
 
   return _tripal_chado_format_term_description($cvterm);
 }
@@ -294,16 +303,57 @@ function _tripal_chado_format_term_description($cvterm) {
   if ($sw_url) {
     $sw_url = preg_replace('/{db}/', $cvterm->dbxref_id->db_id->name, $sw_url);
     $sw_url = preg_replace('/{accession}/', '', $sw_url);
-    $sw_url = url($sw_url, array('absolute' => TRUE));
+    $sw_url = url($sw_url, ['absolute' => TRUE]);
   }
   $vocabulary = tripal_chado_vocab_get_vocabulary($cvterm->dbxref_id->db_id->name);
-  $term = array(
+  $term = [
     'vocabulary' => $vocabulary,
+    'namespace'  => $cvterm->cv_id->name,
     'accession'  => $cvterm->dbxref_id->accession,
     'name'       => $cvterm->name,
     'url'        => chado_get_dbxref_url($cvterm->dbxref_id),
     'definition' => (isset($cvterm->definition)) ? $cvterm->definition : '',
-  );
+  ];
+  
+  // Is this term in any relationships as the subject?
+  if (property_exists($cvterm, 'cvterm_relationship')) {
+    $relationships = $cvterm->cvterm_relationship->subject_id;
+    if ($relationships) {
+      foreach ($relationships as $relationship) {
+        $term['relationship'][] = $relationship->type_id->name . ' ' . $relationship->object_id->dbxref_id->db_id->name . ':' . $relationship->object_id->dbxref_id->accession;
+      }
+    }
+  }
+  
+  // Does this term have synonyms?
+  if (property_exists($cvterm, 'cvtermsynonym')) {
+    $synonyms = $cvterm->cvtermsynonym->cvterm_id;
+    if ($synonyms) {
+      foreach ($synonyms as $synonym) {
+        $term['synonym'][] = $synonym->synonym;
+      }
+    }
+  }
+  
+  // Does this term have any cross refs?
+  if (property_exists($cvterm, 'cvterm_dbxref')) {
+    $xrefs = $cvterm->cvterm_dbxref;
+    if ($xrefs) {
+      foreach ($xrefs as $xref) {
+        $term['cross reference'][] = $xref->dbxref_id->db_id->name . ':' . $xref->dbxref_id->accession;
+      }
+    }
+  }
+  
+  // Does this term have any properties?
+  if (property_exists($cvterm, 'cvtermprop')) {
+    $props = $cvterm->cvtermprop->cvterm_id;
+    if ($props) {
+      foreach ($props as $prop) {
+        $term[$prop->type_id->name][] = property_exists($prop, 'value') ? $prop->value : '';
+      }
+    }
+  }
   return $term;
 }
 

+ 117 - 0
tripal_chado/tripal_chado.install

@@ -1708,3 +1708,120 @@ function tripal_chado_update_7331() {
     throw new DrupalUpdateException('Could not perform update: '. $error);
   }
 }
+
+/**
+ * Adds additional vocabs for the OBO Importer.
+ */
+function tripal_chado_update_7332() {
+  try {
+    // We have to add the db and cv in case the user hasn't yet prepared Chado.
+    // If they have prepared Chado then no harm done.
+    chado_insert_db([
+      'name' => 'NCIT',
+      'description' => 'NCI Thesaurus OBO Edition.',
+      'url' => 'http://purl.obolibrary.org/obo/ncit.owl',
+      'urlprefix' => ' http://purl.obolibrary.org/obo/{db}_{accession}',
+    ]);
+    chado_insert_cv(
+      'ncit',
+      'The NCIt OBO Edition project aims to increase integration of the NCIt with OBO Library ontologies. NCIt is a reference terminology that includes broad coverage of the cancer domain, including cancer related diseases, findings and abnormalities. NCIt OBO Edition releases should be considered experimental.'
+    );
+    
+    $term = chado_insert_cvterm([
+      'id' => 'NCIT:C25693',
+      'name' => 'Subgroup',
+      'cv_name' => 'ncit',
+      'definition' => 'A subdivision of a larger group with members often exhibiting similar characteristics. [ NCI ]',
+    ]);
+    
+    
+    // Add the rdfs:comment vocabulary.
+    chado_insert_db(array(
+      'name' => 'rdfs',
+      'description' => 'Resource Description Framework Schema',
+      'url' => 'https://www.w3.org/TR/rdf-schema/',
+      'urlprefix' => 'http://www.w3.org/2000/01/rdf-schema#{accession}',
+    ));
+    chado_insert_cv(
+      'rdfs',
+      'Resource Description Framework Schema'
+      );
+    $name = chado_insert_cvterm(array(
+      'id' => 'rdfs:comment',
+      'name' => 'comment',
+      'cv_name' => 'rdfs',
+      'definition' => 'A human-readable description of a resource\'s name.',
+    ));
+  }
+  catch (\PDOException $e) {
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not perform update: '. $error);
+  }
+}
+
+/**
+ * Renames the TContact vocabulary database entry to TCONTACT as it should be.
+ */
+function tripal_chado_update_7333() {
+  try {
+    chado_update_record('db', ['name' => 'TContact'], ['name' => 'TCONTACT']);
+  }
+  catch (\PDOException $e) {
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not perform update: '. $error);
+  }
+}
+
+/**
+ * SQL Fix for the db2cv_mview materialized view.
+ */
+function tripal_chado_update_7334() {
+  
+  try {
+    $query = '
+       SELECT DISTINCT CV.cv_id, CV.name as cvname, DB.db_id, DB.name as dbname,
+         COUNT(CVT.cvterm_id) as num_terms
+       FROM cv CV
+         INNER JOIN cvterm CVT on CVT.cv_id = CV.cv_id
+         INNER JOIN dbxref DBX on DBX.dbxref_id = CVT.dbxref_id
+         INNER JOIN db DB on DB.db_id = DBX.db_id
+       WHERE CVT.is_relationshiptype = 0 and CVT.is_obsolete = 0
+       GROUP BY CV.cv_id, CV.name, DB.db_id, DB.name
+       ORDER BY DB.name
+    ';
+    $mview_id = tripal_get_mview_id('db2cv_mview');
+    if($mview_id) {
+      $sql = "UPDATE {tripal_mviews} set query = :query WHERE mview_id = :mview_id";
+      db_query($sql, [':query' => $query, ':mview_id' => $mview_id]);
+    }
+  }
+  catch (\PDOException $e) {
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not perform update: '. $error);
+  }
+}
+/**
+ * SQL Fix for the cv_root_mview materialized view.
+ */
+function tripal_chado_update_7335() {
+  try {
+    $query = '
+      SELECT DISTINCT CVT.name, CVT.cvterm_id, CV.cv_id, CV.name
+      FROM cvterm CVT
+        LEFT JOIN cvterm_relationship CVTR ON CVT.cvterm_id = CVTR.subject_id
+        INNER JOIN cvterm_relationship CVTR2 ON CVT.cvterm_id = CVTR2.object_id
+      INNER JOIN cv CV on CV.cv_id = CVT.cv_id
+      WHERE CVTR.subject_id is NULL and
+        CVT.is_relationshiptype = 0 and CVT.is_obsolete = 0
+      ';
+    $mview_id = tripal_get_mview_id('cv_root_mview');
+    if($mview_id) {
+      $sql = "UPDATE {tripal_mviews} set query = :query WHERE mview_id = :mview_id";
+      db_query($sql, [':query' => $query, ':mview_id' => $mview_id]);
+    }
+  }
+  catch (\PDOException $e) {
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not perform update: '. $error);
+  }
+}

+ 32 - 36
tripal_ws/api/tripal_ws.api.inc

@@ -60,7 +60,7 @@ function hook_tripal_ws_value(&$items, $field, $instance) {
  *
  * @return
  *   A list of TripalWebService names.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_web_services() {
@@ -91,19 +91,15 @@ function tripal_get_web_services() {
  *
  * @return
  *   TRUE if the field type class file was found, FALSE otherwise.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_load_include_web_service_class($class) {
-
   $modules = module_list(TRUE);
   foreach ($modules as $module) {
-    $file_path = realpath(".") . '/' . drupal_get_path('module', $module) . '/includes/TripalWebService/' . $class . '.inc';
-    if (file_exists($file_path)) {
-      module_load_include('inc', $module, 'includes/TripalWebService/' . $class);
-      if (class_exists($class)) {
-        return TRUE;
-      }
+    module_load_include('inc', $module, 'includes/TripalWebService/' . $class);
+    if (class_exists($class)) {
+      return TRUE;
     }
   }
   return FALSE;
@@ -124,7 +120,7 @@ function tripal_load_include_web_service_class($class) {
  *
  * @return
  *   TRUE if the site is successfully added, FALSE otherwise.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_add_site($name, $url, $version, $description) {
@@ -180,7 +176,7 @@ function tripal_add_site($name, $url, $version, $description) {
  *
  * @return
  *   TRUE if the record was successfully deleted, FALSE otherwise.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_remove_site($record_id) {
@@ -196,7 +192,7 @@ function tripal_remove_site($record_id) {
 
 /**
  * Constructs a URL for a remote Tripal web service.
- * 
+ *
  * @param $remote_site
  *   A remote Tripal site object.
  * @param $path
@@ -205,8 +201,8 @@ function tripal_remove_site($record_id) {
  *   leave this paramter empty.
  * @param $query
  *   An query string to appear after the ? in a URL.
- *   
- * @return  
+ *
+ * @return
  *   The full URL within the content service.
  */
 function tripal_build_remote_content_url($remote_site, $path = '', $query = '') {
@@ -215,12 +211,12 @@ function tripal_build_remote_content_url($remote_site, $path = '', $query = '')
   $ws_url = $remote_site->url;
   $ws_url = trim($ws_url, '/');
   $ws_url .= '/web-services/content/' . $ws_version . '/' . $path;
-  
+
   // Build the Query and make and substitions needed.
   if ($query) {
     $ws_url = $ws_url . '?' . $query;
   }
-  
+
   return $ws_url;
 }
 
@@ -238,7 +234,7 @@ function tripal_build_remote_content_url($remote_site, $path = '', $query = '')
  *
  * @return
  *   The JSON response formatted in a PHP array or FALSE if a problem occured.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_content($site_id, $path = '', $query = '') {
@@ -262,7 +258,7 @@ function tripal_get_remote_content($site_id, $path = '', $query = '') {
     _tripal_report_ws_error($data);
     return $data;
   }
-  
+
   // Make the remote query.
   $ws_url = tripal_build_remote_content_url($remote_site, $path, $query);
   $data = drupal_http_request($ws_url);
@@ -287,7 +283,7 @@ function tripal_get_remote_content($site_id, $path = '', $query = '') {
 
   // We got a response, so convert it to a PHP array.
   $data = drupal_json_decode($data->data);
-  
+
   // Check if there was a Tripal Web Services error.
   if (array_key_exists('error', $data)) {
     _tripal_report_ws_error($data);
@@ -298,12 +294,12 @@ function tripal_get_remote_content($site_id, $path = '', $query = '') {
 
 /**
  * A helper function for reporting an error when retrieving remote content.
- * 
+ *
  * @param $data
  *   A data array containing at a minimum the 'error' key containing the
  *   error message.
  */
-function _tripal_report_ws_error($data) {  
+function _tripal_report_ws_error($data) {
   $error = '</pre>' . print_r($data['error'], TRUE) . '</pre>';
   tripal_report_error('tripal_ws', TRIPAL_ERROR,
     'Tripal remote web services reports the following error: !error.',
@@ -323,7 +319,7 @@ function _tripal_report_ws_error($data) {
  * @return
  *   The JSON-LD context mapping array, or FALSE if the context could not
  *   be retrieved.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_context($context_url, $cache_id) {
@@ -374,7 +370,7 @@ function tripal_get_remote_context($context_url, $cache_id) {
  *   the Class (i.e. content type).
  * @return
  *   The JSON-LD context mapping array.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_content_context($site_id, $context_url, $bundle_accession, $field_accession = '') {
@@ -392,7 +388,7 @@ function tripal_get_remote_content_context($site_id, $context_url, $bundle_acces
  *
  * @param $site_id
  *   The numeric site ID for the remote Tripal site.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_clear_remote_cache($site_id) {
@@ -410,7 +406,7 @@ function tripal_clear_remote_cache($site_id) {
  * @return
  *   The vocabulary of a remote Tripal web service, or FALSE if an error
  *   occured.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_API_doc($site_id) {
@@ -498,7 +494,7 @@ function tripal_get_remote_API_doc($site_id) {
  * @return
  *    An array of fake entity objects where the key is the entity_id and
  *    the value is the object.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_load_remote_entities($remote_entity_ids, $site_id, $bundle_accession, $field_ids) {
@@ -531,7 +527,7 @@ function tripal_load_remote_entities($remote_entity_ids, $site_id, $bundle_acces
     '&fields=' . urlencode(implode(",", $field_ids));
 
   $results = tripal_get_remote_content($site_id, $bundle_accession, $query);
-  
+
   // If we encountered an error just return;
   if (array_key_exists('error', $results)) {
     return FALSE;
@@ -591,7 +587,7 @@ function tripal_load_remote_entities($remote_entity_ids, $site_id, $bundle_acces
  *   be added to the entity returned.
  * @return
  *    A fake entity object.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_load_remote_entity($remote_entity_id, $site_id, $bundle_accession, $field_ids) {
@@ -601,7 +597,7 @@ function tripal_load_remote_entity($remote_entity_id, $site_id, $bundle_accessio
 
   // Get the remote entity and create the fake entity.
   $remote_entity = tripal_get_remote_content($site_id, $bundle_accession . '/' . $remote_entity_id);
-  
+
   // If we encountered an error just return;
   if (array_key_exists('error', $results)) {
     return FALSE;
@@ -700,7 +696,7 @@ function tripal_load_remote_entity($remote_entity_id, $site_id, $bundle_accessio
  *   The field array as returned by web services.
  * @param $context
  *   The web service JSON-LD context for the bundle to which the field belongs.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function _tripal_update_remote_entity_field($field_data, $context, $depth = 0) {
@@ -764,7 +760,7 @@ function _tripal_update_remote_entity_field($field_data, $context, $depth = 0) {
  * @return
  *   An array similar to that returned by the field_info_field function
  *   of Drupal for local fields.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_field_info($site_id, $bundle_accession, $field_accession){
@@ -806,7 +802,7 @@ function tripal_get_remote_field_info($site_id, $bundle_accession, $field_access
  * @return
  *   An array similar to that returned by the field_info_instance function
  *   of Drupal for local fields.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_field_instance_info($site_id, $bundle_accession, $field_accession){
@@ -851,7 +847,7 @@ function tripal_get_remote_field_instance_info($site_id, $bundle_accession, $fie
  * @return
  *   A PHP array corresponding to the Hydra Class stanza (i.e. a content type).
  *   Returns NULL if the class ID cannot be found.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_content_doc($site_id, $bundle_accession){
@@ -888,7 +884,7 @@ function tripal_get_remote_content_doc($site_id, $bundle_accession){
  *   A PHP array corresponding to the Hydra property stanza (field) that
  *   belongs to the given Class (i.e. a content type).  Retruns NULL if the
  *   property cannot be found.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_field_doc($site_id, $bundle_accession, $field_accession){
@@ -926,7 +922,7 @@ function tripal_get_remote_field_doc($site_id, $bundle_accession, $field_accessi
  * @return
  *   An array of field downloader class names that are compoatible with the
  *   field and which exist on this site.
- * 
+ *
  * @ingroup tripal_ws_api
  */
 function tripal_get_remote_field_formatters($site_id, $bundle_accession, $field_accession) {
@@ -947,4 +943,4 @@ function tripal_get_remote_field_formatters($site_id, $bundle_accession, $field_
     }
   }
   return $flist;
-}
+}

+ 79 - 32
tripal_ws/includes/TripalFields/remote__data/remote__data.inc

@@ -320,7 +320,7 @@ class remote__data extends WebServicesField {
       ->execute()->fetchAll();
 
     foreach ($sites as $site) {
-      $rows[$site->id] =$site->name;
+      $rows[$site->id] = $site->name;
     }
 
     $element['data_info']['remote_site'] = array(
@@ -333,43 +333,44 @@ class remote__data extends WebServicesField {
     $element['data_info']['query'] = array(
       '#type' => 'textarea',
       '#title' => 'Query to Execute',
-      '#description' => 'Enter the query that will retreive the remote records. ' .
-        'If the full URL to the content web service is ' .
-        'https://[tripal_site]/web-services/content/v0.1/. Then this field should ' .
-        'contain the text immediately after the content/v0.1 portion of the URL. ' .
-        'For information about building web services queries see the ' .
-        'online documentation at ' . l('The Tripal v3 User\'s Guide', 'http://tripal.info/tutorials/v3.x/web-services') . '. ' .
-        'For example, suppose this field is attached to an ' .
-        'Organism content type on the local site, and you want to retrieve a ' .
-        'field for the same organism on a remote Tripal site then you will ' .
-        'want to query on the genus and species. Also, you want the genus and ' .
-        'species to match the organism that this field is attached to. You can ' .
-        'use tokens to do this (see the "Available Tokesn" fieldset below). ' .
-        'For this example, the query text should be ' .
-        'Organism?genus=[taxrank__genus]&species=[taxrank__species].',
+      '#description' => 'Enter the query that will retreive the remote records. ',
       '#default_value' => $this->instance['settings']['data_info']['query'],
-      '#rows' => 5,
-      '#required' => TRUE
-    );
-    $element['data_info']['rd_field_name'] = array(
-      '#type' => 'textfield',
-      '#title' => 'Field to Display',
-      '#description' => 'The results returned by the query should match
-        entities (or records) from the selected remote site.  That entity
-        will have multiple fields. Only one remote field can be shown by
-        this field. Please enter the name of the field you would like
-        to display.  Some fields have "subfields".  You can display a subfield
-        rather than the entire field by entering a comma-separated sequence
-        of subfields.  For example, for relationships, you may only want to
-        show the "clause", therefore, the entry here would be: realtionship,clause.',
-      '#default_value' => $this->instance['settings']['data_info']['rd_field_name'],
+      '#rows' => 3,
       '#required' => TRUE
     );
+    $element['data_info']['query_instructions'] = [
+      '#type' => 'fieldset',
+      '#title' => 'Query to Execute Instructions',
+      '#description' => 'If the full URL to the remote tripal content web '.
+        'service is "https://[tripal_site]/web-services/content/v0.1/". Then '.
+        'this field should contain the text immediately after the '.
+        '"content/v0.1" portion of the URL. For information about building '.
+        'web services queries see the online documentation at '.
+        l('The Tripal v3 User\'s Guide', 'http://tripal.info/tutorials/v3.x/web-services').
+        '. For example, suppose this field is attached to an Organism content '.
+        'type on the local site, and you want to retrieve a field for the '.
+        'same organism on a remote Tripal site. To retrieve the matching '.
+        'record, you will want to query on the genus and species, since it '.
+        'is unique and, you want them to match the organism for each specific '.
+        'local organism page. You can use tokens to do this (see the '.
+        '"Available Tokens" fieldset below). For this example, the full '.
+        'remote web service endpoint would be '.
+        '"https://[tripal_site]/web-services/content/v0.1/Organism" '.
+        'and the query text should be '.
+        '"Organism?genus=[taxrank__genus]&species=[taxrank__species]".',
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE
+    ];
+    $element['data_info']['query_instructions']['instructions'] = [
+      '#type' => 'markup',
+      '#markup' => '',      
+    ];
+    
     $element['data_info']['token_display']['tokens'] = array(
       '#type' => 'hidden',
       '#value' => serialize($tokens)
     );
-
+    
     $element['data_info']['token_display'] = array(
       '#type' => 'fieldset',
       '#title' => 'Available Tokens',
@@ -377,11 +378,57 @@ class remote__data extends WebServicesField {
       '#collapsible' => TRUE,
       '#collapsed' => TRUE
     );
-
+    
     $element['data_info']['token_display']['content'] = array(
       '#type' => 'item',
       '#markup' => theme_token_list($tokens),
     );
+    
+    $element['data_info']['rd_field_name'] = array(
+      '#type' => 'textfield',
+      '#title' => 'Field to Display',
+      '#description' => 'Enter the key from the results returned by the "Query to Execute" that should be displayed. See the example below for more details.',
+      '#default_value' => $this->instance['settings']['data_info']['rd_field_name'],
+      '#required' => TRUE
+    );
+    
+    $element['data_info']['rd_field_name_instructions'] = [
+      '#type' => 'fieldset',
+      '#title' => 'Field to Display Instructions',
+      '#description' => '',
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE
+    ];
+    $element['data_info']['rd_field_name_instructions']['insructions'] = [
+      '#type' => 'markup',
+      '#markup' => 'The query from a Tripal web service response is always
+       in JSON array arranged in key/value pairs.  The key is the name of a
+       controlled vocabulary term.  
+       <br><br>Suppose you want to query details about an organism.
+       Consider the following JSON result 
+       <pre style="height: 200px; overflow: auto;">   
+        "@type": "organism",
+        "label": "Anopheles gambiae",
+        "ItemPage": "http://demo.tripal.info/3.x/bio_data/642",
+        "type": "Organism",
+        "abbreviation": "A.gambiae",
+        "genus": "Anopheles",
+        "species": "gambiae",
+        "common_name": "mosquito",
+        "database_cross_reference": "http://demo.tripal.info/3.x/web-services/content/v0.1/Organism/642/database+cross+reference",
+        "equivalent_name": "Anopheles gambiae sensu stricto",
+        "division": "Invertebrates",
+        "mitochondrial_genetic_code_name": "Invertebrate Mitochondrial",
+        "synonym": "Anopheles gambiae S",
+        "genetic_code": "1",
+        "lineage": "cellular organisms; Eukaryota; Opisthokonta; Metazoa; Eumetazoa; Bilateria; Protostomia; Ecdysozoa; Panarthropoda; Arthropoda; Mandibulata; Pancrustacea; Hexapoda; Insecta; Dicondylia; Pterygota; Neoptera; Holometabola; Diptera; Nematocera; Culicomorpha; Culicoidea; Culicidae; Anophelinae; Anopheles; Cellia; Pyretophorus; gambiae species complex",
+        "genetic_code_name": "Standard",
+        "genbank_common_name": "African malaria mosquito"
+       </pre> 
+       To display the "common_name" from the JSON above you would enter the word
+       "common_name" in the Field to Display textbox.
+      ',
+    ];
 
     $element['data_info']['description'] = array(
       '#type' => 'textarea',

+ 1 - 1
tripal_ws/includes/TripalWebService/TripalContentService_v0_1.inc

@@ -420,7 +420,7 @@ class TripalContentService_v0_1 extends TripalWebService {
         $i++;
       }
       if ($expfield) {
-        $resource = $response;
+        $this->resource = $response;
       }
       else {
         //$this->resource->addProperty($key, $response);

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff