فهرست منبع

Added a new HTML5 file uploader

Stephen Ficklin 8 سال پیش
والد
کامیت
89dd3999f8

+ 296 - 0
tripal/includes/tripal.upload.inc

@@ -0,0 +1,296 @@
+<?php
+
+function tripal_file_upload($type, $filename, $action = NULL, $module = NULL, $chunk = 0) {
+  global $user;
+
+  $user_dir = 'public://tripal/users/' . $user->uid;
+
+  if (!file_prepare_directory($user_dir, FILE_CREATE_DIRECTORY)) {
+    $message = 'Could not access the directory on the server for storing this file.';
+    watchdog('tripal', $message, array(), WATCHDOG_ERROR);
+    drupal_json_output(array(
+      'status'  => 'failed',
+      'message' => $message,
+      'file_id' => '',
+    ));
+    return;
+  }
+
+  // Check to see if this file is uploaded already. If it is
+  // we don't want to allow the file to be uploaded again.
+//   $merge_file = $user_dir . '/' . $filename;
+//   if (file_exists($merge_file)) {
+//     $message = 'This file aready exists on the server and will not be ' .
+//       'uploaded again.',
+//     watchdog('tripal', $message, array(), WATCHDOG_ERROR);
+//     drupal_json_output(array(
+//       'status'  => 'failed',
+//       'message' => $message,
+//       'file_id' => '',
+//     ));
+//     return;
+//   }
+
+  switch ($action) {
+    // If the action is 'put' then the callee is sending a chunk of the file
+    case 'save':
+      tripal_file_upload_put($filename, $chunk, $user_dir);
+      break;
+    case 'check':
+      tripal_file_upload_check($filename, $chunk, $user_dir);
+      break;
+    case 'merge':
+      tripal_file_upload_merge($filename, $type, $user_dir, $module);
+      break;
+  }
+}
+/**
+ * Merges all chunks into a single file
+ * @param unknown $filename
+ */
+function tripal_file_upload_merge($filename, $type, $user_dir, $module) {
+  global $user;
+
+  $status = 'merging';
+  $message = '';
+
+  // Build the paths to the temp directory and merged file.
+  $temp_dir = $user_dir . '/temp' . '/' . $filename;
+  $merge_file = $user_dir . '/' . $filename;
+
+  // If the temp directory where the chunks are found does not exist and the
+  // client is requesting merge then most likely the file has already been
+  // merged and the user hit the upload button again.
+  if (file_exists($temp_dir)) {
+    // Get the upload log.
+    $log = tripal_file_upload_read_log($temp_dir);
+
+    // Keep up with the expected file size.
+    $merge_size = 0;
+
+    // Open the new merged file.
+    $merge_fh = fopen($merge_file, "w");
+    if ($merge_fh){
+      if (flock($merge_fh, LOCK_EX)) {
+        $chunks_written = $log['chunks_written'];
+        $max_chunk = max(array_keys($chunks_written));
+        // Iterate through the chunks in order and see if any are missing.
+        // If so then break out of the loop and fail. Otherwise concatentate
+        // them together.
+        for ($i = 0; $i <= $max_chunk; $i++) {
+          if (!array_key_exists($i, $chunks_written)) {
+            $status = 'failed';
+            $message = 'Missing some chunks. Cannot complete file merge.';
+            break;
+          }
+          $merge_size += $chunks_written[$i];
+          $chunk_file = $temp_dir . '/' . $filename . '.chunk.' . $i;
+          $cfh = fopen($chunk_file, 'r');
+          while ($data = fread($cfh, 1024)) {
+            fwrite($merge_fh, $data);
+          }
+          fclose($cfh);
+        } // end for ($i = 0; $i <= $max_chunk; $i++) { ...
+
+        if (filesize($merge_file) != $merge_size) {
+          $status = 'failed';
+          $message = 'File was uploaded but final merged size is incorrect: ' . $merge_file . '.';
+        }
+      }
+      else {
+        $status = 'failed';
+        $message = 'Cannot lock merged file for writing: ' . $merge_file . '.';
+      }
+    }
+    else {
+      $status = 'failed';
+      $message = 'Cannot open merged file: ' . $merge_file . '.';
+    }
+    fclose($merge_fh);
+  }
+
+  // Make sure the merged file exists
+  if (!file_exists($merge_file)) {
+    $status = 'failed';
+    $message = 'Merge file is missing after upload ' . $merge_file . '.';
+  }
+  $file_id = NULL;
+
+  // If the file has been successfully merged then let the calling module
+  // deal with it.
+  if ($status != 'failed') {
+    $function = $module . '_handle_uploaded_file';
+    if(function_exists($function)) {
+      $file_id = $function($filename, $merge_file, $type);
+      if ($file_id) {
+        $status = 'completed';
+        unlink($temp_dir);
+      }
+      else {
+        $status = 'failed';
+        $message = 'Could not add file to GenSAS database.';
+      }
+    }
+    else {
+      $status = 'failed';
+      $message = 'Requesting module does not implement hook_handle_uploaded_file.';
+    }
+  }
+
+  if ($status == 'failed') {
+    watchdog('tripal', $message, array(), WATCHDOG_ERROR);
+  }
+  drupal_json_output(array(
+    'status'  => $status,
+    'message' => $message,
+    'file_id' => $file_id,
+  ));
+}
+/**
+ * Checks the size of a chunk to see if is fully uploded.
+ *
+ * @return
+ *   returns a JSON array with a status, message and the
+ *   current chunk.
+ */
+function tripal_file_upload_check($filename, $chunk, $user_dir) {
+
+  $chunk_size = $_GET['chunk_size'];
+
+  // Store the chunked file in a temp folder.
+  $temp_dir = $user_dir . '/temp' . '/' . $filename;
+  if (!file_exists($temp_dir)) {
+    mkdir($temp_dir, 0700, TRUE);
+  }
+
+  // Get the upload log.
+  $log = tripal_file_upload_read_log($temp_dir);
+  $chunks_written = $log['chunks_written'];
+  $max_chunk = 0;
+  if ($chunks_written) {
+    $max_chunk = max(array_keys($chunks_written));
+  }
+
+  // Iterate through the chunks in order and see if any are missing.
+  // If so then break out of the loop and this is the chunk to start
+  // on.
+  for ($i = 0; $i <= $max_chunk; $i++) {
+    if (!array_key_exists($i, $chunks_written)) {
+      break;
+    }
+  }
+
+
+  drupal_json_output(array(
+    'status' => 'success',
+    'message' => '',
+    'curr_chunk'  => $i,
+  ));
+}
+/**
+ * Saves the contents of the file being sent to the server.
+ *
+ * The file is saved using the filename the chunk number as an
+ * extension.  This function uses file locking to prevent two
+ * jobs from writing to the same file at the same time.
+ */
+function tripal_file_upload_put($filename, $chunk, $user_dir) {
+
+  // Get the HTTP PUT data.
+  $putdata = fopen("php://input", "r");
+  $size = $_SERVER['CONTENT_LENGTH'];
+
+  // Store the chunked file in a temp folder.
+  $temp_dir = $user_dir . '/temp/' . $filename;
+  if (!file_exists($temp_dir)) {
+    mkdir($temp_dir, 0700, TRUE);
+  }
+
+  // Open the file for writing if doesn't already exist with the proper size.
+  $chunk_file = $temp_dir . '/' . $filename . '.chunk.' . $chunk;
+  if (!file_exists($chunk_file) or filesize($chunk_file) != $size) {
+    // Read the data 1 KB at a time and write to the file
+    $fh = fopen($chunk_file, "w");
+    // Lock the file for writing. We don't want two different
+    // processes trying to write to the same file at the same time.
+    if (flock($fh, LOCK_EX)) {
+      while ($data = fread($putdata, 1024)) {
+        fwrite($fh, $data);
+      }
+      fclose($fh);
+    }
+  }
+  fclose($putdata);
+
+  // Get the current log, updated and re-write it.
+  $log = tripal_file_upload_read_log($temp_dir);
+  $log['chunks_written'][$chunk] = $size;
+  tripal_file_upoad_write_log($temp_dir, $log);
+}
+/**
+ * Reads the upload log file.
+ *
+ * The log file is used to keep track of which chunks have been uploaded.
+ * The format is an array with a key of 'chunks_written' which each element
+ * a key/value pair containing the chunk index as the key and the chunk size
+ * as the value.
+ *
+ * @param $temp_dir
+ *   The directory where the log file will be written. It must be a unique
+ *   directory where only chunks from a single file are kept.
+ */
+function tripal_file_upload_read_log($temp_dir) {
+
+  $log_file = $temp_dir . '/tripal_upload.log';
+  $log = NULL;
+
+  if (file_exists($log_file)) {
+    $fh = fopen($log_file, "r");
+
+    if ($fh and flock($fh, LOCK_EX)) {
+      $contents = '';
+      while ($data = fread($fh, 1024)) {
+        $contents .= $data;
+      }
+      $log = unserialize($contents);
+    }
+    fclose($fh);
+  }
+  if (!is_array($log)) {
+    $log = array(
+      'chunks_written' => array(),
+    );
+  }
+  return $log;
+}
+/**
+ * Writes the upload log file.
+ *
+ * The log file is used to keep track of which chunks have been uploaded.
+ * The format is an array with a key of 'chunks_written' which each element
+ * a key/value pair containing the chunk index as the key and the chunk size
+ * as the value.
+ *
+ * @param $temp_dir
+ *   The directory where the log file will be written. It must be a unique
+ *   directory where only chunks from a single file are kept.
+ * @param $log
+ *   The log array, that is serialized and then written to the file.
+ */
+function tripal_file_upoad_write_log($temp_dir, $log) {
+
+  $log_file = $temp_dir . '/tripal_upload.log';
+
+  if (!$log or !is_array($log)) {
+    $log = array(
+      'chunks_written' => array(),
+    );
+  }
+
+  // Get the last chunk read
+  $fh = fopen($log_file, "w");
+  if ($fh and flock($fh, LOCK_EX)) {
+    fwrite($fh, serialize($log));
+  }
+  fclose($fh);
+}

+ 447 - 0
tripal/theme/js/TripalUploadFile.js

@@ -0,0 +1,447 @@
+/**
+ *  TripalUploadFile Object
+ */
+(function($) {
+
+  "use strict";
+
+  /**
+   * The constructor function.
+   */
+  var TripalUploadFile = function (file, options) {
+   
+    this.file = file;
+    this.options = options;
+    this.file_size = file.size;
+    this.chunk_size = (1024 * 2000); // 2024MB
+    this.total_chunks = ((this.file.size % this.chunk_size == 0) ? Math.floor(this.file.size / this.chunk_size) : Math.floor(this.file.size / this.chunk_size) + 1); 
+    this.curr_chunk = 0;
+    this.status = 'pending';
+    this.file_id = null;
+   
+    if ('mozSlice' in file) {
+      this.slice_method = 'mozSlice';
+    }
+    else if ('webkitSlice' in file) {
+      this.slice_method = 'webkitSlice';
+    }
+    else {
+      this.slice_method = 'slice';
+    }
+
+    var self = this;
+    this.xhr = new XMLHttpRequest();
+    this.xhr.onload = function() {
+      self._onChunkComplete();
+    }
+
+    // Respond to changes in connection
+    if ('onLine' in navigator) {
+      window.addEventListener('online', function () {self._onConnectionFound});
+      window.addEventListener('offline', function () {self._onConnectionLost});
+    }
+  
+
+    // ------------------------------------------------------------------------
+    // Internal Methods 
+    // ------------------------------------------------------------------------
+    /**
+     * 
+     */
+    this._upload = function() {
+      // Cacluate the range for the current chunk
+      var range_start = this.curr_chunk * this.chunk_size;
+      var range_end = range_start + this.chunk_size;
+      
+      // If we've gone beyond the number of chunks then just quit.
+      if (this.curr_chunk > this.total_chunks) {
+        this._onChunkComplete();
+        return;
+      }
+
+      // Prevent range overflow
+      if (this.range_end > this.file_size) {
+        this.range_end = this.file_size;
+      }
+         
+      var chunk = this.file[this.slice_method](range_start, range_end);
+      var url = this.options.url + '/' + this.file.name + '/save/' + this.curr_chunk;
+      
+      this.xhr.open('PUT', url, true);
+      this.xhr.overrideMimeType('application/octet-stream');  
+      this.xhr.setRequestHeader('Content-Range', 'bytes ' + range_start + '-' + range_end + '/' + this.file_size);
+      
+      this.xhr.send(chunk);
+    };
+    
+    /**
+     * Converts a file size into a human readable value.
+     * Borrowed function from:
+     * http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable
+     */
+    this._getReadableSize = function(bytes, si) {
+        var thresh = si ? 1000 : 1024;
+        
+        if(Math.abs(bytes) < thresh) {
+            return bytes + ' B';
+        }
+        var units = si
+          ? ['kB','MB','GB','TB','PB','EB','ZB','YB']
+          : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
+        var u = -1;
+        do {
+          bytes /= thresh;
+          ++u;
+        } 
+        while(Math.abs(bytes) >= thresh && u < units.length - 1);
+        return bytes.toFixed(1) + ' ' + units[u];
+    };
+    
+    /**
+     * Queries server to see what chunk the loading left off at.
+     */
+    this._checkUpload = function() {
+      var url = this.options.url + '/' + this.file.name + '/check/';
+      var self = this;
+      $.ajax({
+        url : url,
+        data : {
+          'chunk_size' : this.chunk_size,
+        },
+        success : function(data, textStatus, jqXHR) {
+          if (data['status'] == 'failed') {
+            self.status = 'failed';
+            self.updateStatus();
+            alert(data['message']);
+          }
+          else {
+            self.curr_chunk =  data['curr_chunk'];
+            self.status = 'uploading';
+            self._upload();
+            self.updateStatus();
+            self.updateProgressBar();
+          }
+        },
+        error : function() {
+          self.curr_chunk = 0;
+          self._upload();
+        }
+      });
+    }
+    
+    /**
+     * 
+     */
+    this._mergeChunks = function() {
+        var url = this.options.url + '/' + this.file.name + '/merge/' + this.options['module'];
+        var self = this;
+        $.ajax({
+          url : url,
+          success : function(data, textStatus, jqXHR) {
+            if (data['status'] == 'completed') {
+              self.file_id = data['file_id'];
+              self.status = 'completed';
+              self.updateStatus();
+            }
+            else {
+              self.status = 'failed';
+              self.updateStatus();
+              alert(data['message']);
+            }
+          },
+          error : function() {
+            self.status = 'failed';
+            self.updateStatus();
+          }
+        });
+    }
+   
+    // ------------------------------------------------------------------------
+    // Event Handlers
+    // ------------------------------------------------------------------------
+   
+    this._onChunkComplete = function() {
+      // If the curr_chunk and the total_chunks is the same then
+      // we've reached the end.
+      if (this.curr_chunk >= this.total_chunks) {
+        this.updateStatus();
+        this._onUploadComplete();
+
+        return;
+      }
+
+      // Continue as long as we aren't paused
+      if (this.status == 'uploading') {
+        this._upload();
+        this.curr_chunk++;
+        this.updateProgressBar();
+      }
+    };
+    /**
+     * 
+     */
+    this._onUploadComplete = function() {
+      this.status = 'merging';
+      this._mergeChunks();
+      this.updateStatus();
+    };
+    /**
+     * When a connection has been lost but reistablished then resume uploads.
+     */
+    this._onConnectionFound = function() {
+      this.resume();
+    };
+ 
+    /**
+     * When a cnnection has been lost then pause uploads.
+     */
+    this._onConnectionLost = function() {
+      this.pause();
+    };
+   
+    // ------------------------------------------------------------------------
+    // Public Methods 
+    // ------------------------------------------------------------------------
+    /**
+     * 
+     */
+    this.getProgressBar = function() {
+      var progress_id = this.options['progress'];
+      return '<div id="' + progress_id + '" class="tripal-uploader-progress-label">0%</div>';
+    };
+    
+    /**
+     * 
+     */
+    this.getLinks = function() {
+      var links_id = this.options['links'];
+      return '<div id="' + links_id + '" class="tripal-uploader-links">0%</div>';
+    }
+    
+    this.getCategory = function() {
+      return this.options['category'];
+    }
+    this.getIndex = function() {
+      return this.options['category'];
+    }
+    this.getTName = function() {
+      return this.options['tname'];
+    }
+    this.getFileName = function() {
+      return this.file.name;
+    }
+    /**
+     * 
+     */
+    this.getFileSize = function(readable) {
+      if (readable) {
+        return this._getReadableSize(this.file.size, true);
+      }
+      else {
+        return this.file.size;
+      }
+    };
+    /**
+     * Updates the links, status text and status bar.
+     */
+    this.updateStatus = function() {
+
+      var progress_id = this.options['progress'];
+      
+      // Add the progress text.
+      $('#' + progress_id).html('');
+      if (this.status == 'cancelled') {
+        $("<span>", {
+          'text' : 'Cancelled',
+        }).appendTo('#' + progress_id)
+      }
+      else if (this.status == 'checking') {
+        $("<span>", {
+          'text' : 'Checking...',
+        }).appendTo('#' + progress_id)
+      }
+      else if (this.status == 'merging') {
+        $("<span>", {
+          'text' : 'Processing...',
+        }).appendTo('#' + progress_id)
+      }
+      else if (this.status == 'failed') {
+        $("<span>", {
+          'text' : 'Failed',
+        }).appendTo('#' + progress_id)
+      }
+      else if (this.status == 'completed') {
+        $("<span>", {
+          'text' : 'Complete',
+        }).appendTo('#' + progress_id)
+        // Set the parent's target field.
+        var parent = self.options['parent'];
+        var tname = self.options['tname'];
+        var category = self.options['category'];
+        parent.setTarget(this.file_id, tname, category);
+      }
+      else if (this.status == 'paused') {
+        $("<span>", {
+          'text' : 'Paused',
+        }).appendTo('#' + progress_id)
+      }
+      
+      // Add a throbber if the status is uploading
+      if (this.status == 'uploading' || this.status == 'checking' || this.status == 'merging') {
+        $("<img>", {
+           'src': tripal_path + '/theme/images/ajax-loader.gif',
+           'class' : 'tripal-chunked-file-progress-throbber',
+         }).appendTo('#' + progress_id);
+      }
+      
+      // Add the appropriate links.
+      var links_id = this.options['links'];
+      var category = this.options['category'];
+      $('#' + links_id).html('');
+      if (this.status == 'cancelled') {
+        $("<a>", {
+          'id': links_id + '-pending',
+          'class': category + '-pending',
+          'href': 'javascript:void(0);',
+          'text': 'Restore',
+        }).appendTo('#' + links_id);
+        $('#' + links_id + '-pending').click(function() {
+          self.pending();
+        })
+      }
+      if (this.status == 'pending') {
+        $("<a>", {
+          'id': links_id + '-cancel',
+          'class': category + '-cancel',
+          'href': 'javascript:void(0);',
+          'text': 'Cancel',
+        }).appendTo('#' + links_id);
+        $('#' + links_id + '-cancel').click(function() {
+          self.cancel();
+        })
+      }
+      if (this.status == 'uploading') {
+        $("<a>", {
+          'id': links_id + '-pause',
+          'class': category + '-pause',
+          'href': 'javascript:void(0);',
+          'text': 'Pause',
+        }).appendTo('#' + links_id);
+        $('#' + links_id + '-pause').click(function() {
+          self.pause();
+        })
+      }
+      if (this.status == 'paused') {
+        $("<a>", {
+          'id': links_id + '-resume',
+          'class': category + '-resume',
+          'href': 'javascript:void(0);',
+          'text': 'Resume',
+        }).appendTo('#' + links_id);
+        $('#' + links_id + '-resume').click(function() {
+          self.resume();
+        })
+      }
+        
+      // Add the remove link.
+      $("<a>", {
+        'id': links_id + '-remove',
+        'class': category + '-remove',
+        'href': 'javascript:void(0);',
+        'text': ' Remove',
+      }).appendTo('#' + links_id);
+      $('#' + links_id + '-remove').click(function() {
+        var parent = self.options['parent'];
+        var index = self.options['index'];
+        var tname = self.options['tname'];
+        var category = self.options['category'];
+        parent.removeFile(category, index);
+        parent.updateTable(category);
+        // Unset the parent's target field.
+        parent.setTarget(null, tname, category);
+        self.cancel();
+      })
+    }
+    /**
+     * Updates the status bar progress only.
+     */
+    this.updateProgressBar = function() {
+      var progress_id = this.options['progress'];
+      var progress = (this.curr_chunk / this.total_chunks) * 100;
+      var self = this;
+
+      // Calculate the amount of the file transferred.
+      var size_transferred = this.curr_chunk * this.chunk_size;
+      size_transferred = this._getReadableSize(size_transferred, true);
+      
+      if (this.status == 'uploading') {
+        $('#' + progress_id).html('');
+        $("<span>", {
+          'class': 'tripal-chunked-file-progress-label',
+          'text': size_transferred,
+        }).appendTo($("<div>", {
+          'id': progress_id + '-bar',
+          'class': 'tripal-chunked-file-progress',
+          'width': progress + '%'
+        }).appendTo($("<div>", {
+          'id': progress_id + '-box',
+          'class': 'tripal-chunked-file-progress',
+        }).appendTo('#' + progress_id)));
+
+      }
+      if (this.status == 'uploading' || this.status == 'checking' || this.status == 'merging') {
+        $("<img>", {
+           'src': tripal_path + '/theme/images/ajax-loader.gif',
+           'class' : 'tripal-chunked-file-progress-throbber',
+         }).appendTo('#' + progress_id);
+      }
+      
+    };
+    /**
+     * 
+     */
+    this.cancel = function() {
+      this.status = 'cancelled';
+      this.updateStatus();
+    }
+    /**
+     * 
+     */
+    this.pending = function() {
+      this.status = 'pending';
+      this.updateStatus();
+    }
+    /**
+     * 
+     */
+    this.start = function() {
+      if (this.status == 'pending') {
+        // Change the status to checking. The first thing we'll
+        // do is see what's already present on the server.
+        this.status = 'checking';
+        this.curr_chunk = this._checkUpload();
+      }
+    };
+
+    /**
+     * 
+     */
+    this.pause = function() {
+      this.status = 'paused';
+      this.updateStatus();
+    };
+    /**
+     * 
+     */
+    this.resume = function() {
+      this.status = 'uploading';
+      this.updateStatus();
+      this.updateProgressBar();
+      this._upload();
+    };
+  };
+  
+  // Export the objects to the window for use in other JS files.
+  window.TripalUploadFile = TripalUploadFile;
+  
+})(jQuery);

+ 578 - 0
tripal/theme/js/TripalUploader.js

@@ -0,0 +1,578 @@
+/**
+ * @file
+ * TripalUploader Object
+ * 
+ * To use the TripalUploader Object the following must be performed:
+ * 
+ * 1) Add a Drupal form to your code that contains the following:
+ *   * A Drupal-style table with 4 or 8 columns.  See the addUploadTable
+ *     function in this class for a description of the columns.
+ *   * A button for submitting a file for upload.
+ * 
+ * @code
+ * $headers = array(
+ *    array('data' => 'Sequence File'),
+ *    array('data' => 'Size', 'width' => '10%'),
+ *    array('data' => 'Upload Progress', 'width' => '20%'),
+ *    array('data' => 'Action', 'width' => '10%')
+ *  );
+ *  $rows = array();
+ *  $table_vars = array(
+ *    'header'      => $headers,
+ *    'rows'        => $rows,
+ *    'attributes'  => array('id' => 'sequence-file-upload-table'),
+ *    'sticky'      => TRUE,
+ *    'colgroups'   => array(),
+ *    'empty'       => t('There are currently no files added.'),
+ *  );
+ *  $form['upload']['sequence_file'] = array(
+ *    '#markup' => theme('table', $table_vars)
+ *  );
+ *  $form['upload']['sequence_fid'] = array(
+ *    '#type' => 'hidden',
+ *    '#value' => 0,
+ *    '#attributes' => array('id' => 'sequence-fid')
+ *  );
+ *  $form['upload']['sequence_file_submit'] = array(
+ *    '#type'     => 'submit',
+ *    '#value'    => 'Upload Sequence File',
+ *    '#name' => 'sequence_file_submit',
+ *    // We don't want this button to submit as the file upload
+ *    // is handled by the JavaScript code.
+ *    '#attributes' => array('onclick' => 'return (false);')
+ *  );
+ * @endcode
+ * 
+ * 
+ * 2)  Edit the theme/js/[module_name].js and in the "Drupal.behaviors.[module]" 
+ * section add a JQuery show function to the form that converts the table 
+ * created in the Drupal form to a TripalUploader table.  The 'table_id' must be
+ * the same as the 'id' attribute set for the table in the Drupal code above.
+ * The 'submit_id' must be the id of the upload button added in the Drupal
+ * code above.  The 'category' for the files.  This is the category that
+ * will be saved in Tripal for the file.  See the addUploadTable function
+ * for additional options.  Include a 'cardinality' setting to indicate
+ * the number of allowed files per upload, and set the 'target_id' to the
+ * name of the field that will contain the file ID (fid) after uploading.
+ * 
+ * @code
+ *  // The TripalUploader object used for uploading of files using the
+ *  // HTML5 File API. Large files are uploaded as chunks and a progress
+ *  // bar is provided.
+ *  var uploader = new TripalUploader();
+ *  
+ *  $('#tripal-sequences-panel-form').show(function() {
+ *    uploader.addUploadTable('sequence_file', {
+ *      'table_id' : '#sequence-file-upload-table',
+ *      'submit_id': '#edit-sequence-file-submit',
+ *      'category' : ['sequence_file'],
+ *      'cardinality' : 1,
+ *      'target_id' : 'sequence-fid',
+ *    });
+ *  });
+ * @endcode
+ *
+ *
+ * 3) Files are uploaded automatically to Tripal.  Files are saved in the
+ * Tripal user's directory.  You can retreive information about the 
+ * file by querying for the file category for the current project.
+ * 
+ * @code
+ *   $seq_files = TripalFeature::getFilesByTypes($user->uid, array('sequence_file'), $project_id);
+ * @endcode
+ * 
+ * 4) If the 'target_id' was used in array for step #2 above, then the 
+ * file ID can be retrieved in the hook_validate() and hook_submit() functions
+ * via the $form_state['input'] array (not the $form_state['values'] array.
+ */
+
+
+(function($) {
+
+  "use strict";
+  
+  /**
+   * The constructor function.
+   */
+  var TripalUploader = function() {
+
+    // Holds the list of files and organizes them by category and then
+    // by an index number.
+    this.files = {};
+    
+    // The tables array will have the following keys:
+    //
+    // tname: the name of the HTML table containing the file.
+    // category:  the category within the table to which the file belongs.
+    // index:  the index of the file in the table.
+    // url: The URL at the remote server where the file will uploaded.
+    this.tables = {};
+
+    /**
+     * Adds a file to the TripalUploader object
+     * 
+     * @param file
+     *   The HTML5 file object.
+     * @param options
+     *   A set of key value pairs of the following
+     *     - tname: the name of the HTML table containing the file.
+     *     - category:  the category within the table to which the file belongs.
+     *     - index:  the index of the file in the table.
+     *     - url: The URL at the remote server where the file will uploaded.
+     */
+    this.addFile = function(file, options) {
+      var tname = options['tname'];
+      var category = options['category'];
+      var i = options['i'];
+      var url = options['url'];
+      var self = this;
+      
+      if (!(category in this.files)) {
+        this.files[category] = {}
+      }      
+      var options = {
+        'parent' : self,
+        'index' : i,
+        'url' : url,
+        'category' : category,
+        'tname' : tname,
+        'progress' : category + '-progress-' + i,
+        'links' : category + '-links-' + i,
+        'module' : this.tables[tname]['module']
+      }
+      var guf = new TripalUploadFile(file, options)
+      this.files[category][i] = guf;
+      return guf
+    };
+    /**
+     * 
+     */
+    this.removeFile = function(category, i) {
+      if (category in this.files) {
+        if (i in this.files[category]) {
+          delete this.files[category][i];
+        }
+      }
+    }
+    /**
+     * 
+     */
+    this.getMaxIndex = function(category) {
+      var index = 0;
+      if (category in this.files) {
+        for (var i in this.files[category]) {
+          if (i > index) {
+            index = i;
+          }
+        }
+      }
+      return index;
+    }
+    /**
+     * 
+     */
+    this.getNumFiles = function(category) {
+      var count = 0;
+      if (category in this.files) {
+        for (var i in this.files[category]) {
+          count = count + 1;
+        }
+      }
+      return count;
+    }
+    /**
+     * 
+     */
+    this.getCategoryFiles = function(category) {
+      if (!(category in this.files)) {
+        return [];
+      }
+      return this.files[category];
+    };
+    /**
+     * 
+     */
+    this.cancelFile = function(category, i) {
+      if (category in this.files) {
+        this.files[category][i].cancel();
+      }
+    };
+    /**
+     * 
+     */
+    this.start = function(category) {
+      if (category in this.files) {
+        for (var i in this.files[category]) {
+          this.files[category][i].start();
+        }
+      }
+    };
+    /**
+     * 
+     */
+    this.updateProgress = function(category) {
+      if (category in this.files) {
+        for (var i in this.files[category]) {
+          this.files[category][i].updateStatus();
+        }
+      }
+    };
+    /**
+     * 
+     */
+    this.reset = function(category) {
+      if (category in this.files) {
+        for (i in this.files[category]) {
+           this.files[category][i].cancel();
+        }
+        this.files[category] = [];
+      }
+    }
+    
+    /**
+     * 
+     */
+    this.getFileButton = function(tname, category, i) {
+      var button_name = tname + '--' + category + '-upload-' + i;
+      var element = '<input id="' + button_name + '" class="tripal-chunked-file-upload" type="file">';
+      
+      return {
+        'name' : button_name,
+        'element' : element,
+      }
+    }
+    
+    /**
+     * 
+     */
+    this.parseButtonID = function(id) {
+      // Get the category and index for this file.
+      var tname = id.replace(/^(.+)--(.+)-upload-(.+)$/, '$1');
+      var category = id.replace(/^(.+)--(.+)-upload-(.+)$/, '$2');
+      var index = id.replace(/^(.+)--(.+)-upload-(.+)$/, '$3');
+      
+      return {
+       'tname' : tname,
+       'category' :  category, 
+       'index' : index
+      };
+    }
+    
+    /**
+     * Adds support for an upload table for a specific category.
+     * 
+     * The TripalUploader supports two types of tables, a table for
+     * uploading paired data (e.g. RNA-seq) and single files.  This function
+     * replaces the body of an existing table as new files and updates
+     * the table as files are uploaded.
+     * 
+     * @param tname
+     *   The name of the table. For single files it is best to name the
+     *   table the same as the file category.  For paired data it is best
+     *   to use a name that represents both categoires.
+     * @param options
+     *   An associative array that contains the following keys:
+     *   table_id: The HTML id of the table.  For single data, the table
+     *     must already have 4 columns with headers (file name,
+     *     size, progress and action). For paired data, the table
+     *     must already have 8 columns, which are the same as the
+     *     single table but with two sets.
+     *   category:  An array. It must contain the list of categories that
+     *     this table manages.  For paired data include two categories.
+     *     This is the category of the file when saved in Tripal.
+     *   submit_id: The HTML id of the submit button.
+     *   module: The name of the module managing the table.
+     *   cardinatily:  (optional) The number of files allowed.  Set to 0 for 
+     *     unlimited.  Defalt is 0.
+     *   target_id: (optional). The HTML id of the hidden field in the form 
+     *     where the file ID will be written to this field. This only 
+     *     works if cardinality is set to 1.
+     *   allowed_types: (optional). An array of allowed file extensions (e.g.
+     *     fasta, fastq, fna, gff3, etc.). 
+     */
+    this.addUploadTable = function(tname, options) {
+      var table_id = options['table_id'];
+      var categories = options['category'];
+      var submit_id = options['submit_id'];
+      var target_id = options['target_id'];
+      var cardinality = options['cardinality'];
+      var module = options['module'];
+      
+      // Save the table ID for this category
+      if (!(tname in this.tables)) {
+        this.tables[tname] = {};
+      }
+      this.tables[tname]['table_id'] = table_id;
+      this.tables[tname]['category'] = categories;
+      this.tables[tname]['submit_id'] = submit_id;
+      this.tables[tname]['target_id'] = target_id;
+      this.tables[tname]['cardinality'] = cardinality;
+      this.tables[tname]['module'] = module;
+      this.updateTable(categories[0]);
+      this.enableSubmit(submit_id);
+    }
+    
+    /**
+     * Adds a click event to the submit button that starts the upload.
+     */
+    this.enableSubmit = function(submit_id) {
+      var self = this;
+      var categories = [];
+      
+      // Iterate through all of the tables that use this submit button
+      // and collect all the categories.  We want to update them all.
+      for (var tname in this.tables) {
+        if (this.tables[tname]['submit_id'] == submit_id){
+          for (var i = 0; i < this.tables[tname]['category'].length; i++) {
+            categories.push(this.tables[tname]['category'][i])
+          } 
+        }
+      }
+      var func_name = ($.isFunction($.fn.live)) ? 'live' : 'on';
+      $(submit_id)[func_name]('click', function() {
+        for(var i = 0; i < categories.length; i++) {
+          self.start(categories[i]);
+        }
+      });
+    }
+    
+    /**
+     * Updates the table for the given file category.
+     */
+    this.updateTable = function(category) {
+      // Iterate through all of the tables that are managed by this object.
+      for (var tname in this.tables) {
+        // Iterate through all of the categories on each table.
+        for (var i = 0; i < this.tables[tname]['category'].length; i++) {
+          // If the category of the table matches then update it.
+          if (this.tables[tname]['category'][i] == category) {
+            // For single files:
+            if (this.tables[tname]['category'].length == 1) {
+              var cat = this.tables[tname]['category'][0];
+              this.updateSingleTable(tname, cat);
+              this.updateProgress(cat);
+              return;
+            }
+            // For paired (e.g. RNA-seq) files:
+            if (this.tables[tname]['category'].length == 2) {
+              var categories = this.tables[tname]['category'];
+              this.updatePairedTable(tname, categories);
+              this.updateProgress(categories[0]);
+              this.updateProgress(categories[1]);
+              return;
+            }
+          }
+        }
+      }
+    }
+
+    /**
+     * A table for non-paired single data.
+     */
+    this.updateSingleTable = function(tname, category) {
+      var i = 0;
+      var content = '';
+      var files  = this.getCategoryFiles(category);
+      var max_index = this.getMaxIndex(category);
+      var has_file = false;
+      var table_id = this.tables[tname]['table_id'];
+      var cardinality = this.tables[tname]['cardinality'];
+      var target_id = this.tables[tname]['target_id'];
+      var num_files = this.getNumFiles(category);
+      var button = null;
+
+      // Build the rows for the non paired samples.
+      has_file = false;
+      for (i = 0; i <= max_index; i++) {
+        button = this.getFileButton(tname, category, i);
+        var trclass = 'odd';
+        if (i % 2 == 0) {
+          trclass = 'even';
+        }
+        content += '<tr class="' + trclass + '">';
+        if (i in files) {
+          content += '<td>' + files[i].file.name + '</td>';
+          content += '<td>' + files[i].getFileSize(true) + '</td>';
+          content += '<td>' + files[i].getProgressBar() + '</td>';
+          content += '<td>' + files[i].getLinks() + '</td>';
+          content += '</tr>';
+          has_file = true;
+        }
+        else {
+          content += '<td colspan="4">' + button['element'] + '</td>';
+        }
+        content +=  '</tr>';
+      }
+
+      // Create an empty row with a file button.
+      if (has_file) {
+        // Only add a new row if we haven't reached our cardinality limit.
+        if (!cardinality || cardinality == 0 || cardinality < num_files) {
+          button = this.getFileButton(tname, category, i);
+          content += '<tr><td colspan="4">' + button['element'] + '</td></tr>';
+        }
+      }
+
+      // Add the body of the table to the table with the provided table_id.
+      $(table_id + ' > tbody').html(content);
+      if (button) {
+        this.enableFileButton(button['name']);
+      }
+    }
+
+    /**
+     * Sets the table's target field with the file id.
+     * 
+     * @param $file_id
+     *   The Tripal file_id
+     * @param $tname
+     *   The name of the HTML table where the file is kept.
+     * @param $category
+     *   The name of the category to which the file belongs.
+     */
+    this.setTarget = function(file_id, tname, category) {
+      var files  = this.getCategoryFiles(category);
+      var cardinality = this.tables[tname]['cardinality'];
+      var target_id = this.tables[tname]['target_id'];
+      var num_files = this.getNumFiles(category);
+         
+      // If cardinality is 1 and this is a single file and we have a
+      // target, then we want to save the file id in the hidden field target 
+      // value
+      if (cardinality && cardinality == 1 && target_id && num_files == 1) {
+        $('#' + target_id).val(files[0].file_id);
+      }
+    }
+
+    /**
+     * A table for paired data (e.g. RNA-seq).
+     */
+    this.updatePairedTable = function(tname, categories) {
+        var i = 0;
+        var table_id = this.tables[tname]['table_id'];
+        var cardinality = this.tables[tname]['cardinality'];
+
+        var category1 = categories[0];
+        var category2 = categories[1];
+
+        var paired_content = '';   
+        var category1_files = this.getCategoryFiles(category1);
+        var category2_files = this.getCategoryFiles(category2);    
+        var max_paired1 = this.getMaxIndex(category1);
+        var max_paired2 = this.getMaxIndex(category2);
+        
+        var button1 = null;
+        var button2 = null;
+
+        // Bulid the rows for the paired sample files table.
+        var has_file = false;
+        for (i = 0; i <= Math.max(max_paired1, max_paired2); i++) {
+          button1 = this.getFileButton(tname, category1, i);
+          button2 = this.getFileButton(tname, category2, i);
+
+          var trclass = 'odd';
+          if (i % 2 == 0) {
+            trclass = 'even';
+          }
+          paired_content +=  '<tr class="' + trclass + '">';
+          if (i in category1_files) {
+            paired_content += '<td>' + category1_files[i].getFileName() + '</td>';
+            paired_content += '<td>' + category1_files[i].getFileSize(true)  + '</td>';
+            paired_content += '<td>' + category1_files[i].getProgressBar() + '</td>';
+            paired_content += '<td>' + category1_files[i].getLinks() + '</td>';
+            has_file = true;
+          }
+          else {
+            paired_content += '<td colspan="4">' + button1['element'] + '</td>';
+          }
+          if (i in category2_files) {
+            paired_content += '<td>' + category2_files[i].getFileName() + '</td>';
+            paired_content += '<td>' + category2_files[i].getFileSize(true) + '</td>';
+            paired_content += '<td>' + category2_files[i].getProgressBar() + '</td>';
+            paired_content += '<td nowrap>' + category2_files[i].getLinks() + '</td>';
+            has_file = true;
+          }
+          else {
+            paired_content += '<td colspan="4">' + button2['element'] + '</td>';
+          }
+          paired_content +=  '</tr>';
+        }
+
+        // Create a new empty row of buttons if we have files.
+        if (has_file) {
+          // Only add a new row if we haven't reached our cardinality limit.
+          if (!cardinality || cardinality == 0 || cardinality < max_index) {
+            button1 = this.getFileButton(tname, category1, i);
+            button2 = this.getFileButton(tname, category2, i);
+            paired_content += '<tr class="odd"><td colspan="4">' + button1['element'] + 
+              '</td><td colspan="4">' + button2['element'] + '</td></tr>'
+          }
+        }
+
+        $(table_id + ' > tbody').html(paired_content);
+        if (button1) {
+          this.enableFileButton(button1['name']);
+        }
+        if (button2) {
+          this.enableFileButton(button2['name']);
+        }
+    }
+
+    /**
+     * Adds a function to the change event for the file button that
+     * causes a new file to be added to this object which it is clicked.
+     * The button is added by the updateUploadTable
+     */
+    this.enableFileButton = function(button_name) {
+        // When the button provided by the TripalUploader class is clicked
+        // then we need to add the files to the object.  We must have this
+        // function so that we can set the proper URL
+        var self = this;
+
+        var func_name = ($.isFunction($.fn.live)) ? 'live' : 'on';
+        $('#' + button_name)[func_name]('change', function(e) {
+          var id = this.id;
+          
+          // Get the HTML5 list of files to upload.
+          var hfiles = e.target.files;
+
+          // Let the TripalUploader object parse the button ID to give us
+          // the proper category name and index.
+          var button = self.parseButtonID(id);
+          var tname = button['tname'];
+          var category = button['category'];
+          var index = button['index'];
+
+          // Add the file(s) to the uploader object.
+          for (var i = 0; i < hfiles.length; i++) {
+            var f = hfiles[i];
+//            if (!f.name.match('^.*\.fastq$')){
+//               alert('Only FastQ files are allowed.');
+//               continue;
+//            }
+            var options = {
+              // Files are managed by tables.
+              'tname' : tname,
+              // Files can be categorized to seprate them from other files.
+              'category': category,
+              // The index is the numeric index of the file. Files are ordered
+              // by their index. The file with an index of 0 is always ordered first.
+              'i': index,
+              // The URL at the remote server where the file will uploaded. 
+              'url' : '/tripal/upload/' + category,
+            };
+            self.addFile(f, options);
+
+            // We need to update the upload table and the progress. The
+            // information for which table to update is in the self.tables
+            // array.
+            self.updateTable(category);
+          }
+        });
+    }
+  };
+
+  // Export the objects to the window for use in other JS files.
+  window.TripalUploader = TripalUploader;
+
+})(jQuery);

+ 8 - 0
tripal/tripal.module

@@ -275,6 +275,14 @@ function tripal_menu() {
     'file path' => drupal_get_path('module', 'tripal'),
     'type' => MENU_LOCAL_ACTION,
   );
+
+  $items['tripal/upload'] = array(
+    'page callback' => 'tripal_file_upload',
+    'access arguments' => array('upload files'),
+    'file' => '/includes/tripal.upload.inc',
+    'type' => MENU_CALLBACK,
+  );
+
   return $items;
 }
 

+ 45 - 0
tripal_chado/api/tripal_chado.entity.api.inc

@@ -0,0 +1,45 @@
+<?php
+/**
+ * A helper function to retreive the entity_id of a given record_id.
+ *
+ * @param $bundle
+ *   A bundle object (as retreieved from tripal_load_bundle_entity().
+ * @param $record_id
+ *   The ID of the record in the Chado table. The record must belong to
+ *   the table to which the bundle is associated in chado.
+ *
+ * @return
+ *   A database result object.
+ */
+function tripal_chado_get_record_entity($bundle, $record_id) {
+  if (!$bundle or !$record_id) {
+    return NULL;
+  }
+  $chado_entity_table = tripal_chado_get_bundle_entity_table($bundle);
+  return db_select($chado_entity_table, 'CE')
+    ->condition('CE.record_id', $record_id)
+    ->execute()
+    ->fetchObject();
+}
+
+/**
+ * A helper function that provides the Chado mapping table for the bundle.
+ *
+ * The tripal_chado module must map entities to their corresponding record
+ * in Chado.  Each bundl type has their own table for this mapping.  This
+ * function provides the name of the table given the bundle name.  A mapping
+ * table will only map to one table in Chado so the record_id's of the mapping
+ * table should always be unique.
+ *
+ * @param $bundle
+ *   A bundle object (as retreieved from tripal_load_bundle_entity().
+ *
+ * @return
+ *   The name of the mapping table that Chado uses to map entities to records.
+ */
+function tripal_chado_get_bundle_entity_table($bundle) {
+  if (!$bundle) {
+    return '';
+  }
+  return 'chado_' . $bundle->name;
+}

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

@@ -1,7 +1,7 @@
 <?php
 
 /**
- * Implements hook_field_create_info().
+ * Implements hook_bundle_create_fields().
  *
  * This is a Tripal defined hook that supports integration with the
  * TripalEntity field.

+ 0 - 1684
tripal_chado/includes/tripal_chado.fields.inc.orig

@@ -1,1684 +0,0 @@
-<?php
-
-/**
- * Implements hook_field_create_info().
- *
- * This is a Tripal defined hook that supports integration with the
- * TripalEntity field.
- */
-function tripal_chado_bundle_create_fields($entity_type, $bundle, $chado_bundle) {
-
-  // Get the details about the mapping of this bundle to the Chado table:
-  $details = array(
-    'chado_cvterm_id' => $chado_bundle['type_id'],
-    'chado_table' => $chado_bundle['data_table'],
-    'chado_type_table' => $chado_bundle['type_linker_table'],
-    'chado_type_column' => $chado_bundle['type_column'],
-  );
-
-  $info = array();
-
-  // Create the fields for each column in the table.
-  tripal_chado_bundle_create_fields_base($info, $details, $entity_type, $bundle);
-
-  // Create custom fields.
-  tripal_chado_bundle_create_fields_custom($info, $details, $entity_type, $bundle);
-
-  // Create fields for linking tables.
-  tripal_chado_bundle_create_fields_linker($info, $details, $entity_type, $bundle);
-
-  foreach ($info as $field_name => $details) {
-    $field_type = $details['type'];
-
-    // If the field already exists then skip it.
-    $field = field_info_field($details['field_name']);
-    if ($field) {
-      continue;
-    }
-
-    // Create the field.
-    $field = field_create_field($details);
-    if (!$field) {
-      tripal_set_message(t("Could not create new field: %field.",
-          array('%field' =>  $details['field_name'])), TRIPAL_ERROR);
-    }
-  }
-}
-/**
- *
- * @param unknown $details
- */
-function tripal_chado_bundle_create_fields_base(&$info, $details, $entity_type, $bundle) {
-
-  $table_name = $details['chado_table'];
-  $type_table = $details['chado_type_table'];
-  $type_field = $details['chado_type_column'];
-  $cvterm_id  = $details['chado_cvterm_id'];
-
-  // Iterate through the columns of the table and see if fields have been
-  // created for each one. If not, then create them.
-  $schema = chado_get_schema($table_name);
-  if (!$schema) {
-    return;
-  }
-
-  $pkey = $schema['primary key'][0];
-
-
-  // Get the list of columns for this table and create a new field for each one.
-  $columns = $schema['fields'];
-  foreach ($columns as $column_name => $details) {
-    // Don't create base fields for the primary key and the type_id field.
-    if ($column_name == $pkey or $column_name == $type_field) {
-      continue;
-    }
-    $cvterm = tripal_get_chado_semweb_term($table_name, $column_name, array('return_object' => TRUE));
-    if (!$cvterm) {
-      tripal_report_error('tripal', TRIPAL_ERROR,
-        'Cannot create field for "%table_name.%column_name". Missing an appropriate vocabulary term',
-         array('%table_name' => $table_name, '%column_name' => $column_name));
-      drupal_set_message(t('Cannot create field for "%table_name.%column_name". Missing an appropriate vocabulary term',
-        array('%table_name' => $table_name, '%column_name' => $column_name)), 'error');
-      continue;
-    }
-    $field_name = strtolower($cvterm->dbxref_id->db_id->name . '__' . preg_replace('/ /', '_', $cvterm->name));
-
-    // Skip the primary key field.
-    if ($column_name == $schema['primary key'][0]) {
-      continue;
-    }
-
-    // Skip the type field.
-    if ($table_name == $type_table and $column_name == $type_field) {
-      continue;
-    }
-
-    // Set some defaults for the field.
-    $base_info = array(
-      'field_name' => $field_name,
-      'type' => '',
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-
-    // Alter the field info array depending on the column details.
-    switch($details['type']) {
-      case 'char':
-        $base_info['type'] = 'text';
-        $base_info['settings']['max_length'] = $details['length'];
-        break;
-      case 'varchar':
-        $base_info['type'] = 'text';
-        $base_info['settings']['max_length'] = $details['length'];
-        break;
-      case 'text':
-        $base_info['type'] = 'text';
-        $base_info['settings']['max_length'] = 17179869184;
-        $base_info['settings']['text_processing'] = 1;
-        break;
-      case 'blob':
-        // not sure how to support a blob field.
-        continue;
-        break;
-      case 'int':
-        $base_info['type'] = 'number_integer';
-        break;
-      case 'float':
-        $base_info['type'] = 'number_float';
-        $base_info['settings']['precision'] = 10;
-        $base_info['settings']['scale'] = 2;
-        $base_info['settings']['decimal_separator'] = '.';
-        break;
-      case 'numeric':
-        $base_info['type'] = 'number_decimal';
-        break;
-      case 'serial':
-        // Serial fields are most likely not needed as a field.
-        break;
-      case 'boolean':
-        $base_info['type'] = 'list_boolean';
-        $base_info['settings']['allowed_values'] = array(0 => "No", 1 => "Yes");
-        break;
-      case 'datetime':
-        // Use the Drupal Date and Date API to create the field/widget
-        $base_info['type'] = 'datetime';
-        break;
-    }
-
-    // Set some default semantic web information
-    if ($column_name == 'uniquename') {
-      $base_info['settings']['text_processing'] = 0;
-    }
-    //
-    // PUB TABLE
-    //
-    elseif ($table_name == 'pub' and $column_name == 'uniquename') {
-      $base_info['type'] = 'text';
-      $base_info['settings']['text_processing'] = 0;
-    }
-
-    //
-    // ANALYSIS TABLE
-    //
-    elseif ($table_name == 'analysis' and $column_name == 'sourceuri') {
-      $base_info['type'] = 'text';
-      $base_info['settings']['text_processing'] = 0;
-    }
-
-    $info[$field_name] = $base_info;
-  }
-}
-
-/**
- *
- * @param unknown $details
- */
-function tripal_chado_bundle_create_fields_custom(&$info, $details, $entity_type, $bundle) {
-  $table_name = $details['chado_table'];
-  $type_table = $details['chado_type_table'];
-  $type_field = $details['chado_type_column'];
-  $cvterm_id  = $details['chado_cvterm_id'];
-
-  $schema = chado_get_schema($table_name);
-
-  // BASE ORGANISM_ID
-  if ($table_name != 'organism' and
-      array_key_exists('organism_id', $schema['fields'])) {
-    $field_name = 'obi__organism';
-    $field_type = 'obi__organism';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // BASE DBXREF
-  if (array_key_exists('dbxref_id', $schema['fields'])) {
-    $field_name = 'data__accession';
-    $field_type = 'data__accession';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // FEATURE MD5CHECKSUM
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence_checksum';
-    $field_type = 'data__sequence_checksum';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // FEATURE RESIDUES
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence';
-    $field_type = 'data__sequence';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // FEATURE SEQLEN
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence_length';
-    $field_type = 'data__sequence_length';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // GENE TRANSCRIPTS
-  $rel_table = $table_name . '_relationship';
-  if (chado_table_exists($rel_table) and $bundle->label == 'gene') {
-    $field_name = 'so__transcript';
-    $field_type = 'so__transcript';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // ORGANISM TYPE_ID
-//   if ($table_name == 'organism' and array_key_exists('type_id', $schema['fields'])) {
-//     $field_name = 'taxarank__infraspecific_taxon';
-//     $field_type = 'taxarank__infraspecific_taxon';
-//     $info[$field_name] = array(
-//       'field_name' => $field_name,
-//       'type' => $field_type,
-//       'cardinality' => 1,
-//       'locked' => TRUE,
-//       'storage' => array(
-//         'type' => 'field_chado_storage',
-//       ),
-//       'settings' => array(
-//       ),
-//     );
-//   }
-}
-
-/**
- *
- * @param unknown $details
- */
-function tripal_chado_bundle_create_fields_linker(&$info, $details, $entity_type, $bundle) {
-
-  $table_name = $details['chado_table'];
-  $type_table = $details['chado_type_table'];
-  $type_field = $details['chado_type_column'];
-  $cvterm_id  = $details['chado_cvterm_id'];
-
-  // CONTACTS
-  $contact_table = $table_name . '_contact';
-  if (chado_table_exists($contact_table)) {
-    $schema = chado_get_schema($contact_table);
-    $pkey = $schema['primary key'][0];
-    $field_name = $table_name . '_contact';
-    $field_type = 'chado_linker__contact';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => 1,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // DBXREF
-  $dbxref_table = $table_name . '_dbxref';
-  if (chado_table_exists($dbxref_table)) {
-    $field_name = 'sbo__database_cross_reference';
-    $field_type = 'sbo__database_cross_reference';
-    $info[$field_name] = array(
-      'field_name' =>  $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // EXPRESSION
-  $expression_table = $table_name . '_expression';
-  if (chado_table_exists($expression_table)) {
-    $field_name = 'go__gene_expression';
-    $field_type = 'go__gene_expression';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // FEATURELOC
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence_coordinates';
-    $field_type = 'data__sequence_coordinates';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // FEATUREPOS
-  if ($table_name == 'feature') {
-    $field_name = 'ogi__location_on_map';
-    $field_type = 'ogi__location_on_map';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // GENOTYPE
-  $genotype_table = $table_name . '_genotype';
-  if (chado_table_exists($genotype_table)) {
-    $field_name = 'so__genotype';
-    $field_type = 'so__genotype';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // PHENOTYPE
-  $phenotype_table = $table_name . '_phenotype';
-  if (chado_table_exists($phenotype_table)) {
-    $field_name = 'sbo__phenotype';
-    $field_type = 'sbo__phenotype';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // PROPERTIES
-  $prop_table = $table_name . 'prop';
-  if (chado_table_exists($prop_table)) {
-    // Get the list of existing property types for this table.
-    $sql = 'SELECT DISTINCT type_id FROM {' . $prop_table . '}';
-    $props = chado_query($sql);
-    while ($prop = $props->fetchObject()) {
-      $term = chado_generate_var('cvterm', array('cvterm_id' => $prop->type_id));
-      $field_name = strtolower(preg_replace('/[^\w]/','_', $term->dbxref_id->db_id->name . '__' . $term->name));
-      $field_type = 'chado_linker__prop';
-      $info[$field_name] = array(
-        'field_name' => $field_name,
-        'type' => $field_type,
-        'cardinality' => 1,
-        'locked' => FALSE,
-        'storage' => array(
-          'type' => 'field_chado_storage',
-        ),
-      );
-    }
-  }
-
-  // PUBLICATIONS
-  $pub_table = $table_name . '_pub';
-  if (chado_table_exists($pub_table)) {
-    $field_name = 'schema__publication';
-    $field_type = 'schema__publication';
-    $info[$field_name] =  array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // RELATIONSHIPS
-  // If the linker table does not exists then we don't want to add attach.
-  $rel_table = $table_name . '_relationship';
-  if (chado_table_exists($rel_table)) {
-    $field_name = 'sbo__relationship';
-    $field_type = 'sbo__relationship';
-    $info[$field_name] =  array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-    );
-  }
-
-  // SYNONYMS
-  $syn_table = $table_name . '_synonym';
-  if (chado_table_exists($syn_table)) {
-    $field_name = 'schema__alternate_name';
-    $field_type = 'schema__alternate_name';
-    $info[$field_name] =  array(
-      'field_name' => $field_name,
-      'type' => $field_type,
-      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-      'locked' => TRUE,
-      'storage' => array(
-        'type' => 'field_chado_storage',
-      ),
-      'settings' => array(
-      ),
-    );
-  }
-}
-
-/**
- * Impelments hook_create_tripalfield_instance().
- *
- * This is a Tripal defined hook that supports integration with the
- * TripalEntity field.
- */
-function tripal_chado_bundle_create_instances($entity_type, $bundle, $chado_bundle) {
-
-  $details = array(
-    'chado_cvterm_id' => $chado_bundle['type_id'],
-    'chado_table' => $chado_bundle['data_table'],
-    'chado_type_table' => $chado_bundle['type_linker_table'],
-    'chado_type_column' => $chado_bundle['type_column'],
-  );
-
-  tripal_chado_bundle_create_instances_base($info, $entity_type, $bundle, $details);
-  tripal_chado_bundle_create_instances_custom($info, $entity_type, $bundle, $details);
-  tripal_chado_bundle_create_instances_linker($info, $entity_type, $bundle, $details);
-
-  foreach ($info as $field_name => $details) {
-    // If the field is already attached to this bundle then skip it.
-    $field = field_info_field($details['field_name']);
-    if ($field and array_key_exists('bundles', $field) and
-        array_key_exists('TripalEntity', $field['bundles']) and
-        in_array($bundle->name, $field['bundles']['TripalEntity'])) {
-      continue;
-    }
-    // Create the field instance.
-    $instance = field_create_instance($details);
-  }
-
-}
-/**
- * Helper function for the hook_create_tripalfield_instance().
- *
- * Add the field instances that corresond to the columns of the base table.
- *
- * @param $entity_type
- * @param $bundle
- * @param $details
- */
-function tripal_chado_bundle_create_instances_base(&$info, $entity_type, $bundle, $details) {
-  $fields = array();
-
-  // Get Chado information
-  $table_name = $details['chado_table'];
-  $type_table = $details['chado_type_table'];
-  $type_field = $details['chado_type_column'];
-  $cvterm_id  = $details['chado_cvterm_id'];
-
-  // Iterate through the columns of the table and see if fields have been
-  // created for each one. If not, then create them.
-  $schema = chado_get_schema($table_name);
-  if (!$schema) {
-    return;
-  }
-
-  $pkey = $schema['primary key'][0];
-
-  $columns = $schema['fields'];
-  foreach ($columns as $column_name => $details) {
-    // Don't create base fields for the primary key and the type_id field.
-    if ($column_name == $pkey or $column_name == $type_field) {
-      continue;
-    }
-    $cvterm = tripal_get_chado_semweb_term($table_name, $column_name, array('return_object' => TRUE));
-    if (!$cvterm) {
-      // We already provided an error when creating the base field.  So
-      // don't create another one here.
-      continue;
-    }
-
-    $field_name = strtolower($cvterm->dbxref_id->db_id->name . '__' . preg_replace('/ /', '_', $cvterm->name));
-
-    // Skip the primary key field.
-    if ($column_name == $schema['primary key'][0]) {
-      continue;
-    }
-
-    // Skip the type field.
-    if ($table_name == $type_table and $column_name == $type_field) {
-      continue;
-    }
-
-    $base_info =  array(
-      'field_name' => $field_name,
-      'entity_type' => 'TripalEntity',
-      'bundle' => $bundle->name,
-      'label' => ucwords(preg_replace('/_/', ' ', $column_name)),
-      'description' => '',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => TRUE,
-        'term_vocabulary' => $cvterm->dbxref_id->db_id->name,
-        'term_name' => $cvterm->name,
-        'term_accession' => $cvterm->dbxref_id->accession,
-        'chado_table' => $table_name,
-        'chado_column' => $column_name,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'settings' => array(),
-        ),
-      ),
-    );
-
-    // Determine if the field is required.
-    if (array_key_exists('not null', $details) and $details['not null'] === TRUE) {
-      $base_info['required'] = TRUE;
-    }
-
-    // Alter the field info array depending on the column details.
-    switch($details['type']) {
-      case 'char':
-        $base_info['widget']['type'] = 'text_textfield';
-        break;
-      case 'varchar':
-        $base_info['widget']['type'] = 'text_textfield';
-        break;
-      case 'text':
-        $base_info['widget']['type'] = 'text_textarea';
-        $base_info['widget']['settings']['format'] = filter_default_format();
-        break;
-      case 'blob':
-        // not sure how to support a blob field.
-        continue;
-        break;
-      case 'int':
-        $base_info['widget']['type'] = 'number';
-        break;
-      case 'float':
-        $base_info['widget']['type'] = 'number';
-        break;
-      case 'numeric':
-        $base_info['widget']['type'] = 'number';
-        break;
-      case 'serial':
-        // Serial fields are most likely not needed as a field.
-        break;
-      case 'boolean':
-        $base_info['widget']['type'] = 'options_onoff';
-        $base_info['required'] = FALSE;
-        break;
-      case 'datetime':
-        $base_info['widget']['type'] = 'date_select';
-        $base_info['widget']['settings']['increment'] = 1;
-        $base_info['widget']['settings']['tz_handling'] = 'none';
-        $base_info['widget']['settings']['collapsible'] = TRUE;
-
-        // TODO: Add settings so that the minutes increment by 1.
-        // And turn off the timezone, as the Chado field doesn't support it.
-        break;
-    }
-
-    // Set some default semantic web information
-    if ($column_name == 'uniquename') {
-      $base_info['label'] = 'Identifier';
-      $base_info['widget_type'] = 'text_textfield';
-    }
-    elseif ($base_info['label'] == 'Timeaccessioned') {
-      $base_info['label'] = 'Time Accessioned';
-      $base_info['description'] = 'Please enter the time that this record was first added to the database.';
-    }
-    elseif ($base_info['label'] == 'Timelastmodified') {
-      $base_info['label'] = 'Time Last Modified';
-      $base_info['description'] = 'Please enter the time that this record was last modified. The default is the current time.';
-    }
-    //
-    // ORGANISM TABLE
-    //
-    elseif ($table_name == 'organism' and $column_name == 'comment') {
-      $base_info['label'] = 'Description';
-    }
-    //
-    // PUB TABLE
-    //
-    elseif ($table_name == 'pub' and $column_name == 'uniquename') {
-      $base_info['widget_type'] = 'text_textfield';
-    }
-
-    //
-    // ANALYSIS TABLE
-    //
-    elseif ($table_name == 'analysis' and $column_name == 'program') {
-      $base_info['description'] = 'The program name (e.g. blastx, blastp, sim4, genscan. If the analysis was not derived from a software package then provide a very brief description of the pipeline, workflow or method.';
-      $base_info['label'] = 'Program, Pipeline, Workflow or Method Name.';
-    }
-    elseif ($table_name == 'analysis' and $column_name == 'sourceuri') {
-      $base_info['widget_type'] = 'text_textfield';
-      $base_info['label'] = 'Source URL';
-      $base_info['description'] = 'The URL where the original source data was derived.  Ideally, this should link to the page where more information about the source data can be found.';
-    }
-    elseif ($table_name == 'analysis' and $column_name == 'sourcename') {
-      $base_info['label'] = 'Source Name';
-      $base_info['description'] = 'The name of the source data. This could be a file name, data set or a small description for how the data was collected. For long descriptions use the larger description field.';
-    }
-    elseif ($table_name == 'analysis' and $column_name == 'sourceversion') {
-      $base_info['label'] = 'Source Version';
-      $base_info['description'] = 'If hte source data set has a version include it here.';
-    }
-    elseif ($table_name == 'analysis' and $column_name == 'algorithm') {
-      $base_info['label'] = 'Source Version';
-      $base_info['description'] = 'The name of the algorithm used to produce the dataset if different from the program.';
-    }
-    elseif ($table_name == 'analysis' and $column_name == 'programversion') {
-      $base_info['label'] = 'Program Version';
-      $base_info['description'] = 'The version of the program used to perform this analysis. (e.g. TBLASTX 2.0MP-WashU [09-Nov-2000]. Enter "n/a" if no version is available or applicable.';
-    }
-    //
-    // PROJECT TABLE
-    //
-    elseif ($table_name == 'project' and $column_name == 'description') {
-      $base_info['label'] = 'Short Description';
-    }
-    $info[$field_name] = $base_info;
-  }
-}
-
-/**
- * Helper function for the hook_create_tripalfield_instance().
- *
- * Adds custom fields for base fields.  These override the settings provided
- * in the tripal_chado_create_tripalfield_instance_base() function.
- *
- * @param $entity_type
- * @param $bundle
- * @param $details
- */
-function tripal_chado_bundle_create_instances_custom(&$info, $entity_type, $bundle, $details) {
-  $table_name = $details['chado_table'];
-  $type_table = $details['chado_type_table'];
-  $type_field = $details['chado_type_column'];
-  $cvterm_id  = $details['chado_cvterm_id'];
-  $schema = chado_get_schema($table_name);
-
-  // BASE ORGANISM_ID
-  if ($table_name != 'organism' and array_key_exists('organism_id', $schema['fields'])) {
-    $field_name = 'obi__organism';
-    $is_required = FALSE;
-    if (array_key_exists('not null', $schema['fields']['organism_id']) and
-        $schema['fields']['organism_id']['not null']) {
-      $is_required = TRUE;
-    }
-    $info[$field_name] =  array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Organism',
-      'description' => 'Select an organism.',
-      'required' => $is_required,
-      'settings' => array(
-        'auto_attach' => TRUE,
-        'chado_table' => $table_name,
-        'chado_column' => 'organism_id',
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'obi__organism_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'type' => 'obi__organism_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // BASE DBXREF
-  if (array_key_exists('dbxref_id', $schema['fields'])) {
-    $field_name = 'data__accession';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Accession',
-      'description' => 'This field specifies the unique stable accession (ID) for
-        this record. It requires that this site have a database entry.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => TRUE,
-        'chado_table' => $table_name,
-        'chado_column' => 'dbxref_id',
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'data__accession_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'type' => 'data__accession_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // FEATURE MD5CHECKSUM
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence_checksum';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Sequence Checksum',
-      'description' => 'The MD5 checksum for the sequence. The checksum here
-        will always be unique for the raw unformatted sequence. To verify that the
-        sequence has not been corrupted, download the raw sequence and use an MD5 tool
-        to calculate the value. If the value calculated is identical the one shown
-        here, then the downloaded sequence is uncorrupted.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => TRUE,
-        'chado_table' => $table_name,
-        'chado_column' => 'md5checksum',
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'data__sequence_checksum_widget',
-        'settings' => array(
-          'display_label' => 1,
-          'md5_fieldname' => 'feature__md5checksum',
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'type' => 'data__sequence_checksum_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // FEATURE RESIDUES
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Sequence',
-      'description' => 'All available sequences for this record.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $table_name,
-        'chado_column' => 'residues',
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'data__sequence_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'data__sequence_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // FEATURE SEQLEN
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence_length';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Sequence Length',
-      'description' => 'The number of residues in the raw sequence.  This length
-        is only for the assigned raw sequence and does not represent the length of any
-        sequences derived from alignments. If this value is zero but aligned sequences
-        are present then this record has no official assigned sequence.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => TRUE,
-        'chado_table' => $table_name,
-        'chado_column' => 'seqlen',
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'data__sequence_length_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'type' => 'data__sequence_length_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // GENE TRANSCRIPTS
-  $rel_table = $table_name . '_relationship';
-  if (chado_table_exists($rel_table) and $bundle->label == 'gene') {
-    $field_name = 'so__transcript';
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Transcripts',
-      'description' => 'Transcripts that are part of this gene.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $rel_table,
-        'chado_column' => '',
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'so__transcript_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'so__transcript_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // ORGANISM TYPE_ID
-//   if ($table_name == 'organism' and array_key_exists('type_id', $schema['fields'])) {
-//     $field_name = 'taxarank__infraspecific_taxon';
-//     $info[$field_name] = array(
-//       'field_name' => $field_name,
-//       'entity_type' => $entity_type,
-//       'bundle' => $bundle->name,
-//       'label' => 'Infraspecific Taxon',
-//       'description' => 'The Infraspecific Taxon.',
-//       'required' => FALSE,
-//       'settings' => array(
-//         'auto_attach' => TRUE,
-//         'chado_table' => 'organism',
-//         'chado_column' => 'type_id',
-//         'base_table' => 'organism',
-//       ),
-//       'widget' => array(
-//         'type' => 'taxarank__infraspecific_taxon_widget',
-//         'settings' => array(
-//           'display_label' => 1,
-//         ),
-//       ),
-//       'display' => array(
-//         'default' => array(
-//           'label' => 'inline',
-//           'type' => 'taxarank__infraspecific_taxon_formatter',
-//           'settings' => array(),
-//         ),
-//       ),
-//     );
-//   }
-}
-
-/**
- *
- * @param unknown $entity_type
- * @param unknown $bundle
- * @param unknown $details
- */
-function tripal_chado_bundle_create_instances_linker(&$info, $entity_type, $bundle, $details) {
-
-  $table_name = $details['chado_table'];
-  $type_table = $details['chado_type_table'];
-  $type_field = $details['chado_type_column'];
-  $cvterm_id  = $details['chado_cvterm_id'];
-
-  // CONTACTS
-  $contact_table = $table_name . '_contact';
-  if (chado_table_exists($contact_table)) {
-    $field_name = $table_name . '_contact';
-    $info[$field_name] =     $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Contact',
-      'description' => 'Associates an indviddual or organization with this record',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $contact_table,
-        'base_table' => $table_name,
-        'chado_column' => 'contact_id',
-      ),
-      'widget' => array(
-        'type' => 'local__contact_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'local__contact_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // DBXREF
-  $dbxref_table = $table_name . '_dbxref';
-  if (chado_table_exists($dbxref_table)) {
-    $field_name = 'sbo__database_cross_reference';
-    $schema = chado_get_schema($dbxref_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] =  array(
-      'field_name' =>  $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Database Cross Reference',
-      'description' => 'The IDs where this record may be available in other external online databases.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $dbxref_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'sbo__database_cross_reference_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'type' => 'sbo__database_cross_reference_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // EXPRESSION
-  $expression_table = $table_name . '_expression';
-  if (chado_table_exists($expression_table)) {
-    $field_name = 'go__gene_expression';
-    $schema = chado_get_schema($expression_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Gene expression',
-      'description' => 'Information about the expression of this record.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $expression_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'go__gene_expression_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'go__gene_expression_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // FEATURELOC
-  if ($table_name == 'feature') {
-    $field_name = 'data__sequence_coordinates';
-    $schema = chado_get_schema('featureloc');
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Sequence Coordinates',
-      'description' => 'The locations on other genomic sequences where this
-        record has been aligned.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => 'featureloc',
-        'chado_column' => $pkey,
-        'base_table' => 'feature',
-      ),
-      'widget' => array(
-        'type' => 'data__sequence_coordinates_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'data__sequence_coordinates_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // FEATUREPOS
-  if ($table_name == 'feature') {
-    $field_name = 'ogi__location_on_map';
-    $schema = chado_get_schema('featurepos');
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Location on Map',
-      'description' => 'The positions on a genetic map.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => 'featurepos',
-        'chado_column' => $pkey,
-        'base_table' => 'feature',
-      ),
-      'widget' => array(
-        'type' => 'ogi__location_on_map_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'ogi__location_on_map_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // GENOTYPE
-  $genotype_table = $table_name . '_genotype';
-  if (chado_table_exists($genotype_table)) {
-    $field_name = 'so__genotype';
-    $schema = chado_get_schema($genotype_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Genotype',
-      'description' => 'The genotypes associated with this record.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $genotype_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'so__genotype_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'so__genotype_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // PHENOTYPE
-  $phenotype_table = $table_name . '_phenotype';
-  if (chado_table_exists($phenotype_table)) {
-    $field_name = 'sbo__phenotype';
-    $schema = chado_get_schema($phenotype_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Phenotype',
-      'description' => 'The phenotypes associated with this record.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $phenotype_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'sbo__phenotype_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'sbo__phenotype_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // PROPERTIES
-  $prop_table = $table_name . 'prop';
-  if (chado_table_exists($prop_table)) {
-     // Get the list of existing property types for this table.
-     $sql = 'SELECT DISTINCT type_id FROM {' . $prop_table . '}';
-     $props = chado_query($sql);
-     $schema = chado_get_schema($prop_table);
-     $pkey = $schema['primary key'][0];
-     while ($prop = $props->fetchObject()) {
-       $term = chado_generate_var('cvterm', array('cvterm_id' => $prop->type_id));
-       $field_name = strtolower(preg_replace('/[^\w]/','_', $term->dbxref_id->db_id->name . '__' . $term->name));
-       $info[$field_name] = array(
-         'field_name' => $field_name,
-         'entity_type' => $entity_type,
-         'bundle' => $bundle->name,
-         'label' => ucwords(preg_replace('/_/', ' ', $term->name)),
-         'description' => $term->definition,
-         'required' => FALSE,
-         'settings' => array(
-           'auto_attach' => TRUE,
-           'term_vocabulary' => $term->dbxref_id->db_id->name,
-           'term_accession' => $term->dbxref_id->accession,
-           'term_name' => $term->name,
-           'base_table' => $table_name,
-           'chado_table' => $prop_table,
-           'chado_column' => $pkey,
-         ),
-         'widget' => array(
-           'type' => 'chado_linker__prop_widget',
-           'settings' => array(
-             'display_label' => 1,
-           ),
-         ),
-         'display' => array(
-           'default' => array(
-             'label' => 'inline',
-             'type' => 'chado_linker__prop_formatter',
-             'settings' => array(),
-           ),
-         ),
-       );
-     }
-   }
-
-
-  // PUBLICATIONS
-  $pub_table = $table_name . '_pub';
-  if (chado_table_exists($pub_table)) {
-    $field_name = 'schema__publication';
-    $schema = chado_get_schema($pub_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Publication',
-      'description' => 'This record has been referenced or is sourced from these publications.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $pub_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'schema__publication_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'schema__publication_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // RELATIONSHIPS
-  // If the linker table does not exists then we don't want to add attach.
-  $rel_table = $table_name . '_relationship';
-  if (chado_table_exists($rel_table)) {
-    $field_name = 'sbo__relationship';
-    $schema = chado_get_schema($rel_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Relationship',
-      'description' => 'Other records with relationships to this record.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $rel_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'sbo__relationship_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'above',
-          'type' => 'sbo__relationship_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-
-  // SYNONYMS
-  $syn_table = $table_name . '_synonym';
-  if (chado_table_exists($syn_table)) {
-    $field_name = 'schema__alternate_name';
-    $schema = chado_get_schema($syn_table);
-    $pkey = $schema['primary key'][0];
-    $info[$field_name] = array(
-      'field_name' => $field_name,
-      'entity_type' => $entity_type,
-      'bundle' => $bundle->name,
-      'label' => 'Synonyms',
-      'description' => 'Alternate names, aliases or synonyms for this record.',
-      'required' => FALSE,
-      'settings' => array(
-        'auto_attach' => FALSE,
-        'chado_table' => $syn_table,
-        'chado_column' => $pkey,
-        'base_table' => $table_name,
-      ),
-      'widget' => array(
-        'type' => 'schema__alternate_name_widget',
-        'settings' => array(
-          'display_label' => 1,
-        ),
-      ),
-      'display' => array(
-        'default' => array(
-          'label' => 'inline',
-          'type' => 'schema__alternate_name_formatter',
-          'settings' => array(),
-        ),
-      ),
-    );
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- *
- * The field_ui_field_overview_form is used for ordering and configuring the
- * fields attached to an entity.
- *
- * This function removes the property adder field as that is really not meant
- * for users to show or manage.
- */
-function tripal_chado_form_field_ui_field_overview_form_alter(&$form, &$form_state, $form_id) {
-
-  // Remove the kvproperty_addr field as it isn't ever displayed. It's just used
-  // on the add/edit form of an entity for adding new property fields.
-  $fields_names = element_children($form['fields']);
-  foreach ($fields_names as $field_name) {
-    $field_info = field_info_field($field_name);
-    if ($field_info['type'] == 'kvproperty_adder') {
-      unset($form['fields'][$field_name]);
-    }
-    if ($field_info['type'] == 'cvterm_class_adder') {
-      unset($form['fields'][$field_name]);
-    }
-  }
-}
-
-/**
- * Implements hook_form_field_ui_field_overview_add_new().
- */
-function tripal_chado_form_field_ui_field_overview_add_new($new_field, $bundle) {
-  // Get the table this bundle is mapped to.
-  $term = tripal_load_term_entity(array('term_id' => $bundle->term_id));
-  $vocab = $term->vocab;
-  $params = array(
-    'vocabulary' => $vocab->vocabulary,
-    'accession' => $term->accession,
-  );
-  $mapped_table = chado_get_cvterm_mapping($params);
-  $chado_table = $mapped_table->chado_table;
-  $chado_type_table = $mapped_table->chado_table;
-  $chado_type_column = $mapped_table->chado_field;
-
-  // We allow site admins to add new chado_linker__prop fields to an entity.
-  // This function will allow us to properly add them.  But at this point we
-  // don't know the controlled vocabulary term.  We'll have to use the
-  // defaults and let the user set it using the interface.
-  if ($new_field['type'] == 'chado_linker__prop') {
-    $table_name = $chado_table . 'prop';
-
-    if (chado_table_exists($table_name)) {
-      $schema = chado_get_schema($table_name);
-      $pkey = $schema['primary key'][0];
-      $field_name = $new_field['field_name'];
-      $field_type = 'chado_linker__prop';
-
-      // First add the field.
-      field_create_field(array(
-        'field_name' => $field_name,
-        'type' => $field_type,
-        'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-        'locked' => FALSE,
-        'storage' => array(
-          'type' => 'field_chado_storage',
-        ),
-        'settings' => array(
-          'base_table' => $chado_table,
-          'chado_table' => $table_name,
-          'chado_column' => $pkey,
-        ),
-      ));
-
-      // Now add the instance
-      field_create_instance(array(
-        'field_name' => $field_name,
-        'entity_type' => 'TripalEntity',
-        'bundle' => $bundle->name,
-        'label' => $new_field['label'],
-        'description' => '',
-        'required' => FALSE,
-        'settings' => array(
-          'auto_attach' => TRUE,
-        ),
-        'widget' => array(
-          'type' => 'chado_linker__prop_widget',
-          'settings' => array(
-            'display_label' => 1,
-          ),
-        ),
-        'display' => array(
-          'default' => array(
-            'label' => 'inline',
-            'type' => 'chado_linker__prop_formatter',
-            'settings' => array(),
-          ),
-        ),
-      ));
-    }
-    else {
-      drupal_set_message('Cannot add a property field to this entity. Chado does not support properties for this data type.', 'error');
-    }
-  }
-
-  // We allow site admins to add new chado_linker__cvterm fields to an entity.
-  // This function will allow us to properly add them.  But at this point we
-  // don't know the controlled vocabulary term.  We'll have to use the
-  // defaults and let the user set it using the interface.
-
-  if ($new_field['type'] == 'chado_linker__cvterm') {
-    $table_name = $chado_table . '_cvterm';
-
-    if (chado_table_exists($table_name)) {
-      $schema = chado_get_schema($table_name);
-      $pkey = $schema['primary key'][0];
-      $field_name = $new_field['field_name'];
-      $field_type = 'chado_linker__cvterm';
-
-      // First add the field.
-      field_create_field(array(
-        'field_name' => $field_name,
-        'type' => $field_type,
-        'cardinality' => FIELD_CARDINALITY_UNLIMITED,
-        'locked' => FALSE,
-        'storage' => array(
-          'type' => 'field_chado_storage',
-        ),
-        'settings' => array(
-          'base_table' => $chado_table,
-          'chado_table' => $table_name,
-          'chado_column' => $pkey,
-        ),
-      ));
-
-      // Now add the instance
-      field_create_instance(array(
-        'field_name' => $field_name,
-        'entity_type' => 'TripalEntity',
-        'bundle' => $bundle->name,
-        'label' => $new_field['label'],
-        'description' => '',
-        'required' => FALSE,
-        'settings' => array(
-          'auto_attach' => TRUE,
-        ),
-        'widget' => array(
-          'type' => 'chado_linker__cvterm_widget',
-          'settings' => array(
-            'display_label' => 1,
-          ),
-        ),
-        'display' => array(
-          'default' => array(
-            'label' => 'above',
-            'type' => 'chado_linker__cvterm_formatter',
-            'settings' => array(),
-          ),
-        ),
-      ));
-    }
-    else {
-      drupal_set_message('Cannot add a property field to this entity. Chado does not support annotations for this data type.', 'error');
-    }
-  }
-}
-
-
-/**
- * Allows for altering of a field's instance setting form.
- *
- * This appears to be a Drupal hook but is actually a custom function created
- * by this module. It is called by the tripal_form_alter() function of this
- * module.
- *
- * Here we put additional form elements for any field, regardless if it is
- * a tripalField or not.
- *
- * @param $form
- *   The form array.  Alterations to the form can be made within this array.
- * @param $form_state
- *   The form state array.
- */
-function tripal_chado_field_instance_settings_form_alter(&$form, $form_state) {
-  global $language;
-  $field = $form['#field'];
-  $instance = $form['#instance'];
-
-  // Only show the Chado mapping information for field instances that map
-  // to Chado.
-  if (!array_key_exists('chado_table',$instance['settings'])) {
-    return;
-  }
-
-  // Construct a table for the vocabulary information.
-  $headers = array();
-  $rows = array();
-  $rows[] = array(
-    array(
-      'data' => 'Base Table',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $instance['settings']['base_table']
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Record Table',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $instance['settings']['chado_table']
-  );
-  $rows[] = array(
-    array(
-      'data' => 'ID Column',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $instance['settings']['chado_column']
-  );
-  $table = array(
-    'header' => $headers,
-    'rows' => $rows,
-    'attributes' => array(
-    ),
-    'sticky' => FALSE,
-    'caption' => '',
-    'colgroups' => array(),
-    'empty' => '',
-  );
-
-  $form['chado_mapping'] = array(
-    '#type' => 'fieldset',
-    '#title' => 'Chado Mapping',
-    '#description' => t('This field maps to data in Chado to the following table:'),
-  );
-  $form['chado_mapping']['details'] = array(
-    '#type' => 'item',
-    '#markup' => theme_table($table),
-  );
-
-}
-<<<<<<< HEAD
-=======
-
-/**
- * Implements hook_form_FROM_ID_alter()
- */
-function tripal_chado_form_tripalbundle_form_alter(&$form, $form_state) {
-  global $language;
-  $bundle = $form['#bundle'];
-
-  $term = entity_load('TripalTerm', array('id' => $bundle->term_id));
-  $term = reset($term);
-  $vocab = $term->vocab;
-  $params = array(
-    'vocabulary' => $vocab->vocabulary,
-    'accession' => $term->accession,
-  );
-  $mapped_table = chado_get_cvterm_mapping($params);
-  $chado_table = $mapped_table->chado_table;
-  $chado_column = $mapped_table->chado_field;
-
-  // Construct a table for the vocabulary information.
-  $headers = array();
-  $rows = array();
-  $rows[] = array(
-    array(
-      'data' => 'Chado Table',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $chado_table
-  );
-  $rows[] = array(
-    array(
-      'data' => 'Type Column',
-      'header' => TRUE,
-      'width' => '20%',
-    ),
-    $chado_column
-  );
-  $table = array(
-    'header' => $headers,
-    'rows' => $rows,
-    'attributes' => array(
-    ),
-    'sticky' => FALSE,
-    'caption' => '',
-    'colgroups' => array(),
-    'empty' => '',
-  );
-
-  $form['chado_mapping'] = array(
-    '#type' => 'item',
-    '#title' => 'Chado Mapping',
-    '#markup' => theme_table($table),
-    '#description' => t('This content type maps to the table in Chado
-        listed above.  Chado allows multiple data types to be housed
-        in a single table. Therefore, the column that is used to
-        differentiate between data types is also listed above.'),
-    '#weight' => 0,
-  );
-}
->>>>>>> a584591704c14877292a457afa4b1e793853f9f6