Browse Source

Merge branch '7.x-3.x' into merger-step2

Lacey Sanderson 5 năm trước cách đây
mục cha
commit
0dffd28774

+ 2 - 0
.gitignore

@@ -5,3 +5,5 @@ vendor/
 _build/
 
 .env
+
+tests/_build

+ 21 - 3
.travis.yml

@@ -8,13 +8,31 @@ services:
   - docker
 
 env:
-  - DRUPAL_ROOT=/var/www/html
+  - DRUPAL_ROOT=/var/www/html JBROWSE_ROOT=/var/www/html/jbrowse IS_TRAVIS=TRUE CC_TEST_REPORTER_ID=1a35bfa282bdb226287eee73c3eb1fdb61f1509117f55417135081624a0366be
 
 before_script:
   - docker pull statonlab/tripal3
+  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
+  - chmod +x ./cc-test-reporter
+  - ./cc-test-reporter before-build --debug
+  - GIT_BRANCH=$TRAVIS_PULL_REQUEST_BRANCH
+  - GIT_COMMIT_SHA=$TRAVIS_PULL_REQUEST_SHA
 
 script:
   - docker run -it -d --rm --name tripal -v "$(pwd)":/modules/tripal_jbrowse statonlab/tripal3
   - sleep 30 # We pause here so postgres and apache complete booting up
-  - docker exec -it tripal drush pm-enable -y tripal_jbrowse
-  - docker exec -it tripal bash -c "cd /modules/tripal_jbrowse && composer install && DRUPAL_ROOT=/var/www/html ./vendor/bin/phpunit"
+  ## Install JBrowse
+  - docker exec -it tripal bash -c 'sudo yum -y groupinstall "Development Tools"'
+  - docker exec -it tripal bash -c 'sudo yum -y install zlib-devel perl-ExtUtils-MakeMaker'
+  - docker exec -it tripal bash -c 'curl -L -O https://github.com/GMOD/jbrowse/releases/download/1.16.6-release/JBrowse-1.16.6.zip'
+  - docker exec -it tripal bash -c 'unzip JBrowse-1.16.6.zip && sudo mv JBrowse-1.16.6 /var/www/html/jbrowse && cd /var/www/html && sudo chown `whoami` jbrowse'
+  - docker exec -it tripal bash -c 'cd /var/www/html/jbrowse && ./setup.sh'
+  ## Setup XDebug for Code Coverage
+  - docker exec -it tripal yum install -y php-pecl-xdebug.x86_64
+  ## Install tripal_jbrowse package.
+  - docker exec -it tripal drush pm-enable -y tripal_jbrowse_page tripal_jbrowse_mgmt
+  ## Run Tests.
+  - docker exec -it tripal bash -c "cd /modules/tripal_jbrowse && composer install && DRUPAL_ROOT=/var/www/html ./vendor/bin/phpunit --coverage-clover ./clover.xml"
+
+after_script:
+  - ./cc-test-reporter after-build clover.xml --debug -t clover -p /var/www/html/sites/all/modules/custom/tripal_jbrowse --exit-code $TRAVIS_TEST_RESULT

+ 7 - 6
README.md

@@ -1,17 +1,18 @@
 # Tripal JBrowse Integration
 
 This package of modules integrates [GMOD JBrowse](https://jbrowse.org/) into your [Tripal](http://tripal.info/) site providing 
- - Tripal page integration via **Tripal JBrowse** and 
+ - Tripal page integration via **Tripal JBrowse Page** and 
  - a user interface for JBrowse instance creation and management via **Tripal JBrowse Management**. 
  
  This powerful combination allows you to provide seamless genome browsing to your users in an administrator-friendly manner.
 
 ## Quick Start
-1. Download and unpack this package in your Drupal modules directory (i.e `sites/all/modules`).
-2. Enable "Tripal JBrowse Management" submodule through `http://[your site]/admin/modules`.
-3. Create a JBrowse instance for your species of interest using the **Admin > Tripal > Tripal JBrowse Management** user interface.
-4. Enable "Tripal JBrowse Integration" submodule through `http://[your site]/admin/modules`.
-5. Create an embeded JBrowse page at **Add Content > JBrowse Instance** by supplying the information for the instance created in step 3.
+1. Download and install JBrowse in a web accessible location (e.g. `DRUPAL_ROOT/tools/jbrowse`).
+2. Download and unpack this package in your Drupal modules directory (i.e `sites/all/modules`).
+3. Enable "Tripal JBrowse Management" submodule through `http://[your site]/admin/modules`.
+4. Create a JBrowse instance for your species of interest using the **Admin > Tripal > Tripal JBrowse Management** user interface.
+5. Enable "Tripal-JBrowse Page Integration" submodule through `http://[your site]/admin/modules`.
+6. Embedded versions will be created for all your instances created in step 4.
 
 ## Documentation
 

+ 22 - 21
composer.lock

@@ -115,27 +115,28 @@
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.3.3",
+            "version": "6.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
+                "reference": "0895c932405407fd3a7368b6910c09a24d26db11"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
-                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0895c932405407fd3a7368b6910c09a24d26db11",
+                "reference": "0895c932405407fd3a7368b6910c09a24d26db11",
                 "shasum": ""
             },
             "require": {
+                "ext-json": "*",
                 "guzzlehttp/promises": "^1.0",
-                "guzzlehttp/psr7": "^1.4",
+                "guzzlehttp/psr7": "^1.6.1",
                 "php": ">=5.5"
             },
             "require-dev": {
                 "ext-curl": "*",
                 "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
-                "psr/log": "^1.0"
+                "psr/log": "^1.1"
             },
             "suggest": {
                 "psr/log": "Required for using the Log middleware"
@@ -147,12 +148,12 @@
                 }
             },
             "autoload": {
-                "files": [
-                    "src/functions_include.php"
-                ],
                 "psr-4": {
                     "GuzzleHttp\\": "src/"
-                }
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
@@ -176,7 +177,7 @@
                 "rest",
                 "web service"
             ],
-            "time": "2018-04-22T15:46:56+00:00"
+            "time": "2019-10-23T15:58:00+00:00"
         },
         {
             "name": "guzzlehttp/promises",
@@ -1754,16 +1755,16 @@
         },
         {
             "name": "symfony/console",
-            "version": "v4.3.4",
+            "version": "v4.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "de63799239b3881b8a08f8481b22348f77ed7b36"
+                "reference": "929ddf360d401b958f611d44e726094ab46a7369"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/de63799239b3881b8a08f8481b22348f77ed7b36",
-                "reference": "de63799239b3881b8a08f8481b22348f77ed7b36",
+                "url": "https://api.github.com/repos/symfony/console/zipball/929ddf360d401b958f611d44e726094ab46a7369",
+                "reference": "929ddf360d401b958f611d44e726094ab46a7369",
                 "shasum": ""
             },
             "require": {
@@ -1825,7 +1826,7 @@
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2019-08-26T08:26:39+00:00"
+            "time": "2019-10-07T12:36:49+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
@@ -2004,16 +2005,16 @@
         },
         {
             "name": "symfony/service-contracts",
-            "version": "v1.1.6",
+            "version": "v1.1.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/service-contracts.git",
-                "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3"
+                "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
-                "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffcde9615dc5bb4825b9f6aed07716f1f57faae0",
+                "reference": "ffcde9615dc5bb4825b9f6aed07716f1f57faae0",
                 "shasum": ""
             },
             "require": {
@@ -2058,7 +2059,7 @@
                 "interoperability",
                 "standards"
             ],
-            "time": "2019-08-20T14:44:19+00:00"
+            "time": "2019-09-17T11:12:18+00:00"
         },
         {
             "name": "theseer/tokenizer",

BIN
docs/assets/instancepage.1.addcontent.png


BIN
docs/assets/instancepage.2.addinstance.png


BIN
docs/assets/instancepage.3.createtop.png


BIN
docs/assets/instancepage.4.createbottom.png


BIN
docs/assets/instancepage.embeddedjbrowse.png


BIN
docs/assets/instancepage.excludesetting.png


BIN
docs/assets/instancepage.linkoriginal.png


+ 18 - 23
docs/guide/instance_page.rst

@@ -1,38 +1,33 @@
-JBrowse Page Integration
+Tripal Page Integration
 ========================
 
-This guide will show you **how to create a page within your Tripal site for an existing JBrowse instance**. This ensures a *consistent user experience* by making the menu system of the Tripal site available to the user while browsing. If the user needs more space they can choose the *FullScreen option* to remove the menus.
+Embedded JBrowse Instance
+---------------------------
 
-.. warning::
-
-   This requires you already have a JBrowse instance. Both local (hosted on the same machine as your Tripal site) or external JBrowse instances are supported.
-
-.. note::
+This guide will show you **how to embed a JBrowse instance within your Tripal site for an existing JBrowse instance**. This ensures a *consistent user experience* by making the menu system of the Tripal site available to the user while browsing. If the user needs more space they can choose the *FullScreen option* to remove the menus.
 
-   The Tripal JBrowse Management sub-module provides a user interface to ease setup of multiple JBrowse instances. It is a great option but is not required.
-
-You create a JBrowse Instance page by navigating to **Content > Add Content > JBrowse Instance** on the Administration Toolbar. Then just fill out the form and click save!
-
-.. image:: ../assets/instancepage.1.addcontent.png
+.. warning::
 
-.. image:: ../assets/instancepage.2.addinstance.png
+   This requires you already have a JBrowse instance created through the Tripal JBrowse Management sub-module. For information on how to do this, see the associated Tripal JBrowse Management documentation.
 
-The **title** will become the title of the page and the **description** will be shown above the JBrowse instances. The description is a good place to add any warnings or instructions.
+This is the default functionality of the "Tripal-JBrowse Page Integration" module. If you have already created your JBrowse instances using the "Tripal Jbrowse Management" module then enabling this module automatically creates pages and menu items for those instances!
 
-.. image:: ../assets/instancepage.3.createtop.png
+.. image:: ../assets/instancepage.embeddedjbrowse.png
 
-The **Existing JBrowse URL** is the URL to the JBrowse instance you want to embed. You should be able to put this URL into your browser and access the JBrowse instance directly, even in the case of local instances.
+Links to Original JBrowse
+---------------------------
 
-The **Start Locations** allows you to specify where you want the JBrowse to navigate to for first time users. Keep in mind that JBrowse caches user location and thus all subsequent times a user accesses the instance it will start at their last browsed location.
+This guide will show you how to ensure the links created by this module direct users to the original JBrowse rather than to an embedded JBrowse page. This may be preferable if your theme does not provide much space to content thus causing the embedded JBrowse to be too small.
 
-.. image:: ../assets/instancepage.4.createbottom.png
+By installing the "Tripal-JBrowse Page Integration" module you will already have embedded JBrowse pages for all Tripal JBrowse instances created by the "Tripal JBrowse Management" module. To switch these links and pages to redirect to the original full-screen JBrowse, go to Administration Toolbar > Tripal > Extensions > Tripal JBrowse Management > Page Integration. **Here you simply need to uncheck the "Embed JBrowse in your site" checkbox and click "Save Settings"**
 
-The **Tracks** allows you to set which tracks you want shown by default. You should enter the machine name of the tracks here with multiple tracks separated by comma's.
+.. image:: ../assets/instancepage.linkoriginal.png
 
-.. note::
+Now, all links in the ``yourdrupalsite.com/jbrowse`` listing and menu items will point the original, full screen JBrowse!
 
-    Depending on how your Drupal site is configured, you may be presented with a **Preview** button instead of a **Save** button. In this case, simply click Preview and then on the next page, click Save.
+Exclude Specific JBrowse Instances
+-----------------------------------
 
-    The preview for the JBrowse may not load properly. Do not be concerned as this is not an indication that you have incorrectly configured the page.
+Sometimes you may want to manage a JBrowse Instance using "Tripal JBrowse Management" but not display it to users through "Tripal-JBrowse Page Integration". For example, if you have just begun development and are not ready to release it. To do this, go to Administration Toolbar > Tripal > Extensions > Tripal JBrowse Management > Page Integration. Check the checkbox beside the instance you would like to exclude from menu items, lists and pages and click "Save Settings".
 
-    To disable the preview button, navigate to Structure > Content Types > JBrowse Instance > edit and set the "Submission for settings" > "Preview before submitting" to "Disabled".
+.. image:: ../assets/instancepage.excludesetting.png

+ 7 - 0
phpunit.xml

@@ -12,4 +12,11 @@
       <directory suffix="Test.php">tests</directory>
     </testsuite>
   </testsuites>
+  <filter>
+     <whitelist addUncoveredFilesFromWhitelist="true">
+       <file>./tripal_jbrowse_mgmt/tripal_jbrowse_mgmt.module</file>
+       <directory suffix=".inc">./tripal_jbrowse_mgmt</directory>
+       <directory suffix=".php">./tripal_jbrowse_mgmt</directory>
+     </whitelist>
+   </filter>
 </phpunit>

+ 0 - 132
tests/tripal_jbrowse/jbrowseInstanceNodeTest.php

@@ -1,132 +0,0 @@
-<?php
-namespace Tests\tripal_jbrowse;
-
-use StatonLab\TripalTestSuite\DBTransaction;
-use StatonLab\TripalTestSuite\TripalTestCase;
-use Faker\Factory;
-
-/**
- * Tests the JBrowse Instance Node Type.
- */
-class jbrowseInstanceNodeTest extends TripalTestCase {
-  // Uncomment to auto start and rollback db transactions per test method.
-  use DBTransaction;
-
-  /**
-   * JBrowse Instance Node Type exists.
-   *
-   * Check that the JBrowse Instance node type exists. It should be created
-   * when the module is installed by the Drupal Node API.
-   */
-  public function testJbrowseInstanceNodeTypeExists() {
-
-    // Get a list of all types available.
-    $types = node_type_get_types();
-
-    // The JBrowse Instance node type must be in the list.
-    $this->assertArrayHasKey('jbrowse_instance', $types, '"JBrowse Instance" node type is not registered with Drupal.');
-
-    // Check that the expected fields exist and are attached to the JBrowse instance node type.
-    // First retrieve all fields for this node type.
-    $fields = field_info_instances('node', 'jbrowse_instance');
-    // Now check that those important to us, exist.
-    $this->assertArrayHasKey('field_jburl', $fields,
-      'The "Existing JBrowse URL" field is not attached to the JBrowse Instance node type.');
-    $this->assertArrayHasKey('field_datadir', $fields,
-      'The "Data Directory" field is not attached to the JBrowse Instance node type.');
-    $this->assertArrayHasKey('field_jbloc', $fields,
-      'The "Start Location" field is not attached to the JBrowse Instance node type.');
-    $this->assertArrayHasKey('field_jbtracks', $fields,
-      'The "Tracks to Display" field is not attached to the JBrowse Instance node type.');
-  }
-
-  /**
-   * Test Creating a JBrowse Instance Node.
-   *
-   * Note: We can't test this by submitting the form via PUT because it requires
-   *  permission to access /node/add/jbrowse_instance; however, we don't yet have a
-   *  way to do this with TripalTestSuite. Furthermore, testing HTTP Requests
-   *  would not give us access to the data added via the test due to database
-   *  transactions.
-   */
-  public function testJBrowseInstanceNodeCreate() {
-    module_load_include('inc', 'node', 'node.pages');
-
-    // Log in the god user.
-    global $user;
-    $user = user_load(1);
-    $node = array('type' => 'jbrowse_instance');
-
-    // Fill in the form.
-    $faker = Factory::create();
-    $form_state = array('values' => array());
-    $form_state['values']['title'] = $faker->words(3, true);
-    $form_state['values']['field_jburl']['und'][0]['url'] = 'https://jbrowse.org/code/JBrowse-1.15.4/';
-    $form_state['values']['field_datadir']['und'][0] = 'sample_data/json/volvox';
-    $form_state['values']['field_jbloc']['und'][0] = 'ctgA:1..11000';
-    $form_state['values']['field_jbtracks']['und'][0] = 'DNA,Genes,volvox-sorted-vcf,volvox_microarray_bw_density,volvox_bb';
-    $form_state['values']['op'] = t('Save');
-
-    // Execute the node creation form.
-    drupal_form_submit('jbrowse_instance_node_form', $form_state, (object) $node);
-
-    // Retrieve any errors.
-    $errors = form_get_errors();
-
-    // Assert that there must not be any.
-    $this->assertEmpty($errors, 'Form submission returned the following errors:'.print_r($errors,TRUE));
-
-    // Check that there is a test jbrowse instance.
-    $result = db_query('SELECT * FROM {node} WHERE title=:name',
-      array(':name' => $form_state['values']['title']));
-    $this->assertEquals(1, $result->rowCount(), 'Unable to select the JBrowse Instance using the name.');
-
-    // log out the god user.
-    $user = drupal_anonymous_user();
-  }
-
-  /**
-   * Update an existing Blast Database Node.
-   */
-  public function testJBrowseInstanceNodeUpdate() {
-    module_load_include('inc', 'node', 'node.pages');
-
-    // Log in the god user.
-    global $user;
-    $user = user_load(1);
-
-    // Create the node in the first place.
-    $seeder = \Tests\DatabaseSeeders\JBrowseInstanceNodeSeeder::seed();
-    $node = $seeder->getNode();
-
-    // Now use the form to edit it :-)
-    // Specifically, we will change the name, url and data directory.
-    $faker = Factory::create();
-    $form_state = array('values' => array());
-    $form_state['values']['title'] = $faker->words(5, true);
-    $form_state['values']['field_jburl']['und'][0]['url'] = 'https://jbrowse.org/code/JBrowse-1.15.4/';
-    $form_state['values']['field_datadir']['und'][0] = 'sample_data/json/modencode';
-    $form_state['values']['op'] = t('Save');
-
-    // Execute the node creation form.
-    drupal_form_submit('jbrowse_instance_node_form', $form_state, $node);
-
-    // Retrieve any errors.
-    $errors = form_get_errors();
-    // Assert that there must not be any.
-    $this->assertEmpty($errors, 'Form submission returned the following errors:'.print_r($errors,TRUE));
-
-    // Check that there is a test jbrowse instance.
-    $result = db_query('SELECT * FROM {node} WHERE title=:name',
-      array(':name' => $form_state['values']['title']));
-    $this->assertEquals(1, $result->rowCount(), 'Unable to select the JBrowse Instance using the name.');
-
-    // log out the god user.
-    $user = drupal_anonymous_user();
-  }
-
-  /**
-   * Test deleting a node.
-   * NOTE: We cannot test this via drupal_form_submit() since it requires a confirmation.
-   */
-}

+ 279 - 0
tests/tripal_jbrowse_mgmt/ApiTest.php

@@ -0,0 +1,279 @@
+<?php
+namespace Tests\tripal_jbrowse_mgmt;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+use Faker\Factory;
+
+class ApiTest extends TripalTestCase {
+  // Uncomment to auto start and rollback db transactions per test method.
+  use DBTransaction;
+
+  /**
+   * Tests the Settings API.
+   *
+   * Specifically tripal_jbrowse_mgmt_save_settings()
+   * and tripal_jbrowse_mgmt_get_settings().
+   */
+  public function testSettings() {
+
+    $test_settings = [
+      'bin_path' => 'test/fake/path',
+	    'link' => 'test/fake/path',
+	    'data_dir' => 'test/fake/path',
+	    'menu_template' => [],
+    ];
+
+    tripal_jbrowse_mgmt_save_settings($test_settings);
+    $retrieved = tripal_jbrowse_mgmt_get_settings();
+
+    // Check that all the expected settings are available.
+    $expected = ['bin_path', 'link', 'data_dir', 'menu_template'];
+    foreach ($expected as $expected_key) {
+      $this->assertArrayHasKey($expected_key, $retrieved,
+        "Retrieved settings do not contain $expected_key");
+
+      // Check that the settings match if provided in test.
+      if (isset($test_settings[$expected_key])) {
+        $this->assertEquals(
+          $test_settings[$expected_key],
+          $retrieved[$expected_key],
+          "The retrieved value for $expected_key does not match what we set."
+        );
+      }
+    }
+  }
+
+  /**
+   * Test Instance Create-Retrieve-Update-Delete.
+   */
+  public function testInstanceCRUD() {
+    putenv("TRIPAL_SUPPRESS_ERRORS=TRUE");
+
+    $organism = factory('chado.organism')->create();
+
+    // Full Fake Instance Details.
+    $faker = Factory::create();
+    $testdata = [
+      'organism_id' => $organism->organism_id,
+      'title' => $faker->words(3, TRUE),
+      'description' => $faker->sentence(25, TRUE),
+      'created_at' => $faker->unixTime(),
+      'file' => '/path/to/fake/file',
+    ];
+
+    // Check we cannot create a JBrowse instance without an organism.
+    $noOrganism = $testdata;
+    unset($noOrganism['organism_id']);
+    $id = tripal_jbrowse_mgmt_create_instance($noOrganism);
+    $this->assertFalse($id,
+      "Created an instance without an organism_id!?!");
+
+    // Now try to create an instance with all the data.
+    $id = tripal_jbrowse_mgmt_create_instance($testdata);
+    $this->assertNotFalse($id,
+      "Unable to create instance.");
+
+    // Try to retrieve the instance we just created.
+    $retrieved_instance = tripal_jbrowse_mgmt_get_instance($id);
+    $this->assertNotFalse($id, "We did not retrieve an instance?");
+    $this->assertEquals($id, $retrieved_instance->id,
+      "We retrieved a different instance then we asked for?");
+    $this->assertEquals(
+      $testdata['organism_id'], $retrieved_instance->organism_id,
+      "Retreived the same instance but the organism is not correct?");
+
+    // Change the title and test the instance was updated.
+    $new_title = $testdata;
+    $testdata['title'] = 'NEW FAKE TITLE ' . uniqid();
+    $success = tripal_jbrowse_mgmt_update_instance($id, $new_title);
+    $this->assertNotFalse($success, 'Unable to update instance title.');
+    $retrieved_instance = tripal_jbrowse_mgmt_get_instance($id);
+    $this->assertEquals(
+      $new_title['title'], $retrieved_instance->title,
+      "The title was not updated.");
+
+    // Finally delete him!
+    $success = tripal_jbrowse_mgmt_delete_instance($retrieved_instance);
+    $this->assertNotFalse($success, 'Unable to delete this instance.');
+    $one_more_time = tripal_jbrowse_mgmt_get_instance($id);
+    $this->assertFalse($one_more_time);
+
+    // and delete with numeric id.
+    $success = tripal_jbrowse_mgmt_delete_instance($id);
+    $this->assertNotFalse($success, 'Unable to delete this instance.');
+    // and catch an exception during delete.
+    try {
+      $success = tripal_jbrowse_mgmt_delete_instance('FAKESTRING');
+      // If we get here without an exception then this test should fail.
+      $this->assertTrue(FALSE, "Shouldn't be able to extract instance to delete.");
+    } catch (\Exception $e) {
+        // Not worried since this is expected!
+    }
+
+    putenv("TRIPAL_SUPPRESS_ERRORS");
+  }
+
+  /**
+   * Test Track Create-Retrieve-Update-Delete.
+   */
+  public function testTrackCRUD() {
+
+    // Fake Instance.
+    $faker = Factory::create();
+    $organism = factory('chado.organism')->create();
+    $instance_details = [
+      'organism_id' => $organism->organism_id,
+      'title' => $faker->words(3, TRUE),
+      'description' => $faker->sentence(25, TRUE),
+      'created_at' => $faker->unixTime(),
+      'file' => '/path/to/fake/file',
+    ];
+    $instance_id = tripal_jbrowse_mgmt_create_instance($instance_details);
+    $instance = tripal_jbrowse_mgmt_get_instance($instance_id);
+
+    $path = tripal_jbrowse_mgmt_get_track_list_file_path($instance);
+    $this->assertNotFalse($path,
+      "Unable to retrieve the path to the trackList.json");
+
+    // Fake track details.
+    $testdata = [
+      'label' => $faker->words(2, TRUE),
+      'track_type' => 'CanvasFeatures',
+      'file_type' => 'gff',
+      'created_at' => $faker->unixTime(),
+      'file' => '/path/to/fake/file',
+    ];
+
+    // First retrieve when there are no tracks.
+    $tracks = tripal_jbrowse_mgmt_get_tracks($instance);
+    $this->assertCount(0, $tracks,
+      "There should not be tracks as we just created this instance.");
+
+    // Create tracks.
+    $track_id = tripal_jbrowse_mgmt_create_track($instance, $testdata);
+    $this->assertNotFalse($track_id,
+      "We should have a track created successfully.");
+
+    // Retrieve our newly created track.
+    $track = tripal_jbrowse_mgmt_get_track($track_id);
+    $this->assertNotFalse($track, "Unable to create track.");
+    $this->assertEquals($testdata['label'], $track->label,
+      "The label of our new track didn't match what we submitted.");
+    $this->assertEquals($instance->id, $track->instance_id,
+      "The instance_id of our new track didn't match what we submitted.");
+    $this->assertEquals($instance->organism_id, $track->organism_id,
+      "The organism_id of our new track didn't match what we submitted.");
+
+    // Retrieve track with a condition.
+    $tracks = tripal_jbrowse_mgmt_get_tracks($instance, ['label' => $testdata['label']]);
+    $this->assertCount(1, $tracks, "We were not able to select a track we knew should exist.");
+
+    // Now update it.
+    $new_label = 'NEW LABEL ' . uniqid();
+    $success = tripal_jbrowse_mgmt_update_track($track, ['label' => $new_label]);
+    $this->assertNotFalse($success, "Unable to update track label.");
+    $new_track = tripal_jbrowse_mgmt_get_track($track_id);
+    $this->assertEquals($new_label, $new_track->label,
+      "The label of the track was not updated.");
+
+    // Finally, delete it.
+    $success = tripal_jbrowse_mgmt_delete_track($track_id);
+    $this->assertNotFalse($success, "Unable to delete track.");
+    $success = tripal_jbrowse_mgmt_delete_track($track_id);
+    $this->assertEquals(0, $success, "Deleted a track that doesn't exist?");
+  }
+
+  /**
+   * Test organism-related api functions.
+   */
+  public function testOrganismAPI() {
+
+    // Fake Organism.
+    $faker = Factory::create();
+    $organism = factory('chado.organism')->create();
+
+    $organism_list = tripal_jbrowse_mgmt_get_organisms_list();
+    $this->assertNotCount(0, $organism_list, "There should be at least one organism.");
+    $this->assertArrayContainsObjectValue($organism_list, 'organism_id', $organism->organism_id);
+
+    $organism_name = tripal_jbrowse_mgmt_construct_organism_name($organism);
+    $this->assertRegexp('/'.$organism->genus.'/', $organism_name,
+      "The organism name did not contain the genus.");
+    $this->assertRegexp('/'.$organism->species.'/', $organism_name,
+      "The organism name did not contain the species.");
+
+    $slug = tripal_jbrowse_mgmt_make_slug($organism_name);
+    $this->assertNotRegexp('/ /', $slug,
+      "The organism slug should not contain spaces.");
+  }
+
+  /**
+   * Test Instance Properties Create-Retrieve-Update.
+   */
+  public function testInstanceProperyCRU() {
+
+    // Fake Instance.
+    $faker = Factory::create();
+    $organism = factory('chado.organism')->create();
+    $instance_details = [
+      'organism_id' => $organism->organism_id,
+      'title' => $faker->words(3, TRUE),
+      'description' => $faker->sentence(25, TRUE),
+      'created_at' => $faker->unixTime(),
+      'file' => '/path/to/fake/file',
+    ];
+    $instance_id = tripal_jbrowse_mgmt_create_instance($instance_details);
+
+    $testdata = [
+      $faker->word() => $faker->words(3, TRUE),
+      $faker->word() => $faker->words(2, TRUE),
+      $faker->word() => $faker->words(10, TRUE),
+    ];
+
+    // Create.
+    tripal_jbrowse_mgmt_save_instance_properties($instance_id, $testdata);
+
+    // Retrieve.
+    $properties = tripal_jbrowse_mgmt_get_instance_properties($instance_id);
+    $this->assertIsArray($properties, "Unable to retrieve newly created properties.");
+    $this->assertCount(3, $properties,
+      "There was not the expected count of properties.");
+
+    // Update single property.
+    $property_name = key($testdata);
+    $new_value = 'NEW VALUE '.uniqid();
+    tripal_jbrowse_mgmt_save_instance_property($instance_id, $property_name, $new_value);
+    $retrieved_value = tripal_jbrowse_mgmt_get_instance_property($instance_id, $property_name);
+    $this->assertEquals($new_value, $retrieved_value,
+      "We were unable to update a single property.");
+
+  }
+
+  /**
+   * Test Miscellaneous API functions.
+   */
+  public function testMiscAPI() {
+
+    $track_types = tripal_jbrowse_mgmt_get_track_types();
+    $this->assertIsArray($track_types,
+      "The track types should be an array.");
+
+  }
+  /**
+   * Provide an assertion to check properties of an array of objects.
+   *
+   * For example, if you have an array of organism objects, the following code
+   * would check that one object in the array has an organism_id of 50.
+   * @code $this->assertArrayContainsObjectValue($array, 'organism_id', 50);
+   */
+  private function assertArrayContainsObjectValue($theArray, $attribute, $value)
+  {
+      foreach($theArray as $arrayItem) {
+          if($arrayItem->$attribute == $value) {
+              return true;
+          }
+      }
+      return false;
+  }
+}

+ 43 - 0
tests/tripal_jbrowse_mgmt/ModuleFileTest.php

@@ -0,0 +1,43 @@
+<?php
+namespace Tests\tripal_jbrowse_mgmt;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+
+class ModuleFileTest extends TripalTestCase {
+  // Uncomment to auto start and rollback db transactions per test method.
+  // use DBTransaction;
+
+  /**
+   * Tests hook_menu(). Specifically, are all the required keys set.
+   */
+  public function testHookMenu() {
+
+    $menu_items = tripal_jbrowse_mgmt_menu();
+    $this->assertIsArray($menu_items);
+
+    foreach($menu_items as $path => $item) {
+      $this->assertArrayHasKey('title', $item,
+        "$path menu item is missing a title.");
+      $this->assertArrayHasKey('page callback', $item,
+        "$path menu item is missing a page callback.");
+      $this->assertArrayHasKey('access arguments', $item,
+        "$path menu item is missing access arguments.");
+      $this->assertIsArray($item['access arguments'],
+        "$path menu item access arguments must be an array.");
+    }
+  }
+
+  /**
+   * Tests hook_permission(). Specifically, checks format.
+   */
+  public function testHookPerm() {
+    $permissions = tripal_jbrowse_mgmt_permission();
+    $this->assertIsArray($permissions);
+
+    foreach ($permissions as $key => $perm) {
+      $this->assertArrayHasKey('title', $perm,
+        "$key permission is missing a title.");
+    }
+  }
+}

+ 33 - 0
tests/tripal_jbrowse_page/dotModuleTest.php

@@ -0,0 +1,33 @@
+<?php
+namespace Tests\tripal_jbrowse_page;
+
+use StatonLab\TripalTestSuite\DBTransaction;
+use StatonLab\TripalTestSuite\TripalTestCase;
+
+class dotModuleTest extends TripalTestCase {
+  // Uncomment to auto start and rollback db transactions per test method.
+  use DBTransaction;
+
+  /**
+   * Tests tripal_jbrowse_page_menu().
+   */
+  public function testHookMenu() {
+
+    $menu_items = tripal_jbrowse_page_menu();
+    foreach ($menu_items as $path => $item) {
+
+      // First ensure there are all the menu item keys required.
+      $this->assertArrayHasKey('title', $item);
+      $this->assertArrayHasKey('description', $item);
+      $this->assertArrayHasKey('page callback', $item);
+      $this->assertArrayHasKey('type', $item);
+      $this->assertArrayHasKey('access arguments', $item);
+
+      // Check that all jbrowse instance paths are the correct type.
+      $path_parts = explode('/', $path);
+      if (($path_parts[0] == 'jbrowse') && (sizeof($path_parts) > 1)) {
+        $this->assertEquals(MENU_SUGGESTED_ITEM, $item['type']);
+      }
+    }
+  }
+}

+ 2 - 3
tripal_jbrowse/tripal_jbrowse.info

@@ -1,7 +1,6 @@
-name = Tripal-JBrowse Integration
-description = Integrates existing JBrowse instances into Tripal sites.
+name = JBrowse Nodes DEPRECATED
+description = Creates nodes embedding JBrowse instances.
 core = 7.x
 package = Tripal Extensions
 
-dependencies[] = link
 dependencies[] = tripal

+ 17 - 13
tripal_jbrowse_mgmt/includes/tripal_jbrowse_mgmt.api.inc

@@ -93,7 +93,22 @@ function tripal_jbrowse_mgmt_get_instances($conditions = NULL) {
 function tripal_jbrowse_mgmt_create_instance($data) {
   global $user;
 
-  $instance_id = db_insert('tripal_jbrowse_mgmt_instances')->fields([
+  // Check that required fields are provided.
+  $required = ['organism_id', 'created_at'];
+  foreach ($required as $key) {
+    if (!isset($data[$key])) {
+      tripal_report_error(
+        'tripal_jbrowse_mgmt',
+        TRIPAL_ERROR,
+        'Unable to create instance due to missing :key',
+        [':key' => $key]
+      );
+      return FALSE;
+    }
+  }
+
+  // If they are all present, then insert into the database.
+  return db_insert('tripal_jbrowse_mgmt_instances')->fields([
     'uid' => $user->uid,
     'organism_id' => $data['organism_id'],
     'title' => $data['title'],
@@ -102,11 +117,6 @@ function tripal_jbrowse_mgmt_create_instance($data) {
     'file' => $data['file'],
   ])->execute();
 
-  if (!$instance_id) {
-    return FALSE;
-  }
-
-  return $instance_id;
 }
 
 /**
@@ -171,7 +181,7 @@ function tripal_jbrowse_mgmt_delete_instance($instance) {
 function tripal_jbrowse_mgmt_create_track($instance, $data) {
   global $user;
 
-  $track_id = db_insert('tripal_jbrowse_mgmt_tracks')->fields([
+  return db_insert('tripal_jbrowse_mgmt_tracks')->fields([
     'uid' => $user->uid,
     'instance_id' => $instance->id,
     'organism_id' => $instance->organism_id,
@@ -181,12 +191,6 @@ function tripal_jbrowse_mgmt_create_track($instance, $data) {
     'created_at' => $data['created_at'],
     'file' => $data['file'],
   ])->execute();
-
-  if (!$track_id) {
-    return FALSE;
-  }
-
-  return $track_id;
 }
 
 /**

+ 86 - 0
tripal_jbrowse_page/includes/tripal_jbrowse_page.admin.inc

@@ -0,0 +1,86 @@
+<?php
+/**
+ * @file
+ * Administration (settings) for this module.
+ */
+
+/**
+ * Settings Form for page integration.
+ */
+function tripal_jbrowse_page_settings_form($form, $form_state) {
+
+  $form['general'] = [
+    '#type' => 'fieldset',
+    '#title' => 'General Page Integration Settings'
+  ];
+
+  $form['general']['embed'] = [
+    '#type' => 'checkbox',
+    '#title' => 'Embed JBrowse in your Tripal Site',
+    '#description' => 'Check this box if you would like to embed the JBrowse in your Tripal site. Conversely, uncheck it for links to go directly to the full-screen JBrowse.',
+  '#default_value' => variable_get('trpjbrowse_page_embed', 1),
+  ];
+
+  $form['exclude_fieldset'] = [
+    '#type' => 'fieldset',
+    '#title' => 'Exclude JBrowse Instances from Page Integration',
+    '#description' => 'To exclude a JBrowse instance from page integration (i.e. no embedded page will be shown and it won\'t show up in the listing), check the checkbox below.',
+  ];
+
+  // Get all instances as an option to be excluded.
+  $instances = tripal_jbrowse_mgmt_get_instances();
+  $options = [];
+  foreach ($instances as $instance) {
+    $key = $instance->id;
+    $options[$key] = [
+      'genus' => $instance->organism->genus,
+      'species' => $instance->organism->species,
+    ];
+  }
+  // Determine the default value.
+  $default_exclude = variable_get('trpjbrowse_page_exclude', []);
+  if (!is_array($default_exclude)) {
+    $default_exclude = unserialize($default_exclude);
+  }
+  // Now generate the table.
+  $form['exclude_fieldset']['exclude'] = [
+    '#type' => 'tableselect',
+    '#header' => [
+      'genus' => t('Genus'),
+      'species' => t('Species'),
+    ],
+    '#options' => $options,
+    '#empty' => t('No JBrowse Instances available. Please add one through the "List Instances" page.'),
+    '#default_value' => $default_exclude,
+  ];
+
+  $form['submit'] = [
+    '#type' => 'submit',
+    '#value' => 'Save Settings',
+  ];
+
+  return $form;
+}
+
+/**
+ * Settings Form for page integration.
+ */
+function tripal_jbrowse_page_settings_form_submit($form, $form_state) {
+  $values = $form_state['values'];
+
+  // General Settings.
+  variable_set(
+    'trpjbrowse_page_embed',
+    $values['embed']
+  );
+
+  // Exclude Instances.
+  variable_set(
+    'trpjbrowse_page_exclude',
+    serialize($values['exclude'])
+  );
+
+  // Ensure the menu is rebuilt.
+  variable_set('menu_rebuild_needed', TRUE);
+
+}

+ 53 - 0
tripal_jbrowse_page/includes/tripal_jbrowse_page.api.inc

@@ -0,0 +1,53 @@
+<?php
+/**
+ * @file
+ * Contains Application Programmer Interface (API) functions for this module.
+ */
+
+/**
+ * Retrieve the instance id based on the organism.
+ */
+function tripal_jbrowse_page_get_instance_id($conditions, $options) {
+
+  // First retrieve the organism_id.
+  $organism_id = chado_query('SELECT organism_id FROM {organism} WHERE genus=:genus AND species=:species',
+    [
+      ':genus' => $conditions['genus'],
+      ':species' => $conditions['species']
+    ])->fetchField();
+
+  // Then retrieve the instance for that organism.
+  if ($options['load_instance']) {
+    $instances = tripal_jbrowse_mgmt_get_instances(['organism_id' => $organism_id]);
+    return $instances[0];
+  }
+  else {
+    return db_select('tripal_jbrowse_mgmt_instances', 'I')
+      ->fields('I', ['id'])
+      ->condition('organism_id', $organism_id)
+      ->execute()->fetchField();
+  }
+
+}
+
+/**
+ * Retrieve the instance id based on the organism.
+ */
+function tripal_jbrowse_page_is_instance_public($instance_id) {
+  $excluded_instances = variable_get('trpjbrowse_page_exclude', []);
+  if (!is_array($excluded_instances)) {
+    $excluded_instances = unserialize($excluded_instances);
+  }
+
+  if (isset($excluded_instances[$instance_id])) {
+    if ($excluded_instances[$instance_id] === $instance_id) {
+      return FALSE;
+    }
+    else {
+      return TRUE;
+    }
+  }
+  else {
+    return TRUE;
+  }
+}

+ 30 - 0
tripal_jbrowse_page/includes/tripal_jbrowse_page.listing.inc

@@ -0,0 +1,30 @@
+<?php
+/**
+ * @file
+ * Builds the public jbrowse listing.
+ */
+
+/**
+ * Builds the public jbrowse listing.
+ */
+function tripal_jbrowse_page_listing_page() {
+  $settings = tripal_jbrowse_mgmt_get_settings();
+
+  // Retrieve all the instances...
+  $instances = tripal_jbrowse_mgmt_get_instances();
+
+  drupal_add_css(drupal_get_path('module', 'tripal_jbrowse_page') . '/theme/tripal_jbrowse_page.css');
+
+  // Add the URL for each to link to.
+  foreach($instances as $k => $instance) {
+    if (tripal_jbrowse_page_is_instance_public($instance->id)) {
+      $instances[$k]->url = url('jbrowse/'.$instance->organism->genus . '-' . $instance->organism->species, ['absolute' => TRUE]);
+    }
+    else {
+      unset($instances[$k]);
+    }
+  }
+
+  // Use the template to render the page.
+  return theme('jbrowse_instance_public_listing', ['instances' => $instances]);
+}

+ 36 - 0
tripal_jbrowse_page/includes/tripal_jbrowse_page.page.inc

@@ -0,0 +1,36 @@
+<?php
+/**
+ * @file
+ * Builds the Tripal JBrowse page.
+ */
+
+/**
+ * Redirect to the JBrowse Instance.
+ */
+function tripal_jbrowse_page_page($scientific_name) {
+  list($genus, $species) = explode('-', $scientific_name);
+  $instance = tripal_jbrowse_page_get_instance_id([
+    'genus' => $genus,
+    'species' => $species
+  ],
+  ['load_instance' => TRUE]);
+
+  // Determine Query paramters.
+  $query_params = tripal_jbrowse_mgmt_build_http_query($instance);
+  $page_q = drupal_get_query_parameters();
+  foreach ($page_q as $qkey => $qvalue) {
+    $query_params[$qkey] = $qvalue;
+  }
+
+  // Build the URL.
+  $settings = tripal_jbrowse_mgmt_get_settings();
+  $url = url($settings['link'],['query' => $query_params]);
+
+  if (variable_get('trpjbrowse_page_embed', 1)) {
+    drupal_add_css(drupal_get_path('module', 'tripal_jbrowse_page') . '/theme/tripal_jbrowse_page.css');
+    return theme('jbrowse_instance_embedded_page', ['url' => $url]);
+  }
+  else {
+    drupal_goto($url, array('external' => TRUE));
+  }
+}

+ 5 - 0
tripal_jbrowse_page/theme/jbrowse-instance--embedded.tpl.php

@@ -0,0 +1,5 @@
+
+<div id="JBrowseInstance">
+  <iframe src="<?php print $url;?>" width="100%" height="100%">
+  </iframe>
+</div>

+ 32 - 0
tripal_jbrowse_page/theme/jbrowse-instance--public-listing.tpl.php

@@ -0,0 +1,32 @@
+<p>The JBrowse genome browser allows you to visually explore genomes and their associated large-scale datasets. JBrowse is a widely used application that is fast, intuitive, and compatible with most web browsers.</p>
+
+<div class="jbrowse-list">
+
+  <?php foreach ($instances as $instance) { ?>
+
+    <div class="jbrowse-instance">
+      <h3><?php print l($instance->title, $instance->url); ?></h3>
+      <p><?php print $instance->description; ?></p>
+      <span class="jbrowse-launch-link"><?php print l('Launch JBrowse', $instance->url); ?></span>
+    </div>
+
+  <?php }
+    if (empty($instances)) {?>
+
+      <div class="empty-list">
+        <p>There are currently no available JBrowse instances.</p>
+      </div>
+  <?php } ?>
+</div>
+
+
+<div class="jbrowse-admin-message">
+  <?php
+    print tripal_set_message(
+      'You can create or register a JBrowse Instance at '
+      .l('Administration Toolbar > Tripal > Extensions > Tripal JBrowse Management', 'admin/tripal/extension/tripal_jbrowse/management'),
+      TRIPAL_INFO,
+      ['return_html' => TRUE]
+    );
+  ?>
+</div>

+ 23 - 0
tripal_jbrowse_page/theme/tripal_jbrowse_page.css

@@ -0,0 +1,23 @@
+
+/** JBrowse Listing **/
+.jbrowse-list {
+  border-top: 1px solid #d0d0d0;
+  margin-top: 20px;
+}
+.jbrowse-list .jbrowse-instance {
+  border-bottom: 1px solid #d0d0d0;
+  margin-bottom: 20px;
+  padding-bottom: 30px;
+}
+.jbrowse-list .jbrowse-instance .jbrowse-launch-link {
+  float: right;
+}
+.jbrowse-list .empty-list {
+  margin-top: 20px;
+  font-style: italic;
+}
+
+/** Embedded Page **/
+#JBrowseInstance iframe {
+  min-height: 700px;
+}

+ 7 - 0
tripal_jbrowse_page/tripal_jbrowse_page.info

@@ -0,0 +1,7 @@
+name = Tripal-JBrowse Page Integration
+description = Integrates JBrowse instances into Tripal Pages.
+core = 7.x
+package = Tripal Extensions
+
+dependencies[] = tripal
+dependencies[] = tripal_jbrowse_mgmt

+ 78 - 0
tripal_jbrowse_page/tripal_jbrowse_page.module

@@ -0,0 +1,78 @@
+<?php
+
+// API.
+require_once 'includes/tripal_jbrowse_page.api.inc';
+
+/**
+ * Implements hook_menu().
+ */
+function tripal_jbrowse_page_menu() {
+  $items = [];
+
+  // Listing Page.
+  $items['jbrowse'] = [
+    'title' => 'JBrowse',
+    'description' => 'A listing of available JBrowse instances.',
+    'page callback' => 'tripal_jbrowse_page_listing_page',
+    'access arguments' => ['access content'],
+    'file' => 'includes/tripal_jbrowse_page.listing.inc',
+    'type' => MENU_NORMAL_ITEM,
+  ];
+
+  // Pages for each JBrowse Instance.
+  $instances = tripal_jbrowse_mgmt_get_instances();
+  foreach ($instances as $instance) {
+
+    if (tripal_jbrowse_page_is_instance_public($instance->id)) {
+
+      // Create the menu item.
+      $path = 'jbrowse/'.$instance->organism->genus . '-' . $instance->organism->species;
+      $items[$path] = [
+        'title' => $instance->title,
+        'description' => $instance->description,
+        'page callback' => 'tripal_jbrowse_page_page',
+        'page arguments' => [1],
+        'access arguments' => ['access content'],
+        'file' => 'includes/tripal_jbrowse_page.page.inc',
+        'type' => MENU_SUGGESTED_ITEM,
+      ];
+    }
+  }
+
+  // Administration.
+  $admin_path = 'admin/tripal/extension/tripal_jbrowse/management';
+  $items[$admin_path.'/page-integration'] = [
+    'title' => 'Page Integration',
+    'description' => 'A listing of available JBrowse instances.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => ['tripal_jbrowse_page_settings_form'],
+    'access arguments' => ['access content'],
+    'file' => 'includes/tripal_jbrowse_page.admin.inc',
+    'type' => MENU_LOCAL_TASK,
+  ];
+
+  return $items;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function tripal_jbrowse_page_theme($existing, $type, $theme, $path) {
+
+  $items = array(
+    'jbrowse_instance_public_listing' => array(
+      // Don't specify the path in the template name.
+      // Unless you have your template inside a directory within this module.
+      'template' =>  'theme/jbrowse-instance--public-listing',
+      'variables' => array('instances' => []),
+    ),
+    'jbrowse_instance_embedded_page' => array(
+      // Don't specify the path in the template name.
+      // Unless you have your template inside a directory within this module.
+      'template' =>  'theme/jbrowse-instance--embedded',
+      'variables' => array('url' => ''),
+    ),
+  );
+
+  return $items;
+}