Browse Source

Initial import of version 0.2

spficklin 15 years ago
parent
commit
2e4465fa79

+ 12 - 0
tripal_feature/tripal_feature.info

@@ -0,0 +1,12 @@
+; $Id: tripal_feature.info,v 1.4 2009/10/01 17:52:24 ccheng Exp $
+name = Tripal Chado Feature
+description = A module for interfacing the GMOD chado database with Drupal, providing viewing, inserting and editing of chado features.
+core = 6.x
+project = tripal_feature
+package = Tripal
+dependencies[] = tripal_core
+dependencies[] = tripal_organism
+dependencies[] = search
+dependencies[] = path
+dependencies[] = tripal_cv
+version = "6.x-0.2b-m0.2"

+ 154 - 0
tripal_feature/tripal_feature.install

@@ -0,0 +1,154 @@
+<?php
+
+/************************************************************************
+*  Implementation of hook_install();
+*/
+function tripal_feature_install(){
+   // create the module's data directory
+   tripal_create_moddir('tripal_feature');
+
+   // create the tables that correlate drupal nodes with chado 
+   // features, organisms, etc....
+   drupal_install_schema('tripal_feature');
+  
+   // add the materialized view
+   tripal_feature_add_organism_count_mview();
+}
+/*******************************************************************************
+*  Update for Drupal 6.x, Tripal 0.2b, Feature Module 0.2
+*  This update adjusts the materialized view by adding a 'cvterm_id' column
+*/
+
+function tripal_feature_update_6000(){
+   // recreate the materialized view
+   tripal_feature_add_organism_count_mview();
+   $ret = array(
+      '#finished' => 1,
+   );
+   
+   return $ret;
+}
+/*******************************************************************************
+* Implementation of hook_schema().
+*/
+function tripal_feature_add_organism_count_mview(){
+   $view_name = 'organism_feature_count';
+
+   // Drop the MView table if it exists
+   $mview_id = tripal_mviews_get_mview_id($view_name);
+   if($mview_id){
+      tripal_mviews_action("delete",$mview_id);
+   }
+
+   // Create the MView
+   tripal_add_mview(
+      // view name
+      $view_name,
+      // tripal module name
+      'tripal_feature',
+      // table name
+      $view_name,
+      // table schema definition
+      'organism_id integer, genus character varying(255), '.
+      '  species character varying(255), '.
+      '  common_name character varying(255), '.
+      '  num_features integer, cvterm_id integer, '.
+      '  feature_type character varying(255)',
+      // columns for indexing
+      'organism_id,cvterm_id,feature_type',
+      // SQL statement to populate the view
+      'SELECT O.organism_id, O.genus, O.species, O.common_name, 
+          count(F.feature_id) as num_features, 
+          CVT.cvterm_id, CVT.name as feature_type 
+       FROM {Organism} O 
+          INNER JOIN Feature F           ON O.Organism_id = F.organism_id 
+          INNER JOIN Cvterm CVT          ON F.type_id = CVT.cvterm_id 
+       GROUP BY O.Organism_id, O.genus, O.species, O.common_name,
+          CVT.cvterm_id, CVT.name',
+      // special index
+      ''
+   );
+
+   // add a job to the job queue so this view gets updated automatically next
+   // time the job facility is run
+   $mview_id = tripal_mviews_get_mview_id($view_name);
+   if($mview_id){
+      tripal_mviews_action('update',$mview_id);
+   }
+}
+/************************************************************************
+* Implementation of hook_schema().
+*/
+function tripal_feature_schema() {
+   $schema = tripal_feature_get_schemas();
+   return $schema;
+}
+/************************************************************************
+* Implementation of hook_uninstall()
+*/
+function tripal_feature_uninstall(){
+
+   // Drop the MView table if it exists
+   $mview_id = tripal_mviews_get_mview_id('organism_feature_count');
+   if($mview_id){
+      tripal_mviews_action("delete",$mview_id);
+   }
+
+   drupal_uninstall_schema('tripal_feature');
+
+   // Get the list of nodes to remove
+   $sql_feature_id = "SELECT nid, vid ".
+                 "FROM {node} ".
+                 "WHERE type='chado_feature'";
+   $result = db_query($sql_feature_id);
+   while ($node = db_fetch_object($result)) {
+      node_delete($node->nid);
+   }
+}
+
+/************************************************************************
+* This function simply defines all tables needed for the module to work
+* correctly.  By putting the table definitions in a separate function we 
+* can easily provide the entire list for hook_install or individual
+* tables for an update.
+*/
+function tripal_feature_get_schemas (){  
+  $schema = array();
+
+  $schema['chado_feature'] = array(
+      'fields' => array(
+         'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
+         'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
+         'feature_id' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+         'sync_date' => array ('type' => 'int', 'not null' => FALSE, 'description' => 'UNIX integer sync date/time'),
+      ),
+      'indexes' => array(
+         'feature_id' => array('feature_id')
+       ),
+      'unique keys' => array(
+         'nid_vid' => array('nid','vid'),
+         'vid' => array('vid')
+      ),
+      'primary key' => array('nid'),
+  );
+
+  return $schema;
+}
+
+/*******************************************************************************
+ * Implementation of hook_requirements(). Make sure 'Tripal Core' is enabled
+ * before installation
+ */
+function tripal_feature_requirements($phase) {
+   $requirements = array();
+   if ($phase == 'install') {
+      if (!function_exists('tripal_create_moddir')) {
+         $requirements ['tripal_feature'] = array(
+            'title' => "tripal_feature",
+            'value' => "error. Some required modules are just being installed. Please try again.",
+            'severity' => REQUIREMENT_ERROR,
+         );
+      }
+   }
+   return $requirements;
+}

+ 1975 - 0
tripal_feature/tripal_feature.module

@@ -0,0 +1,1975 @@
+<?php
+
+//
+// Copyright 2009 Clemson University
+//
+
+/*************************************************************************
+*
+*/
+function tripal_feature_init(){
+
+   // add the jGCharts JS and CSS
+   drupal_add_js (drupal_get_path('theme', 'tripal').'/js/tripal_feature.js'); 
+   drupal_add_js (drupal_get_path('theme', 'tripal').'/js/jgcharts/jgcharts.js'); 
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_admin () {
+
+   // before proceeding check to see if we have any
+   // currently processing jobs. If so, we don't want
+   // to give the opportunity to sync libraries
+   $active_jobs = FALSE;
+   if(tripal_get_module_active_jobs('tripal_feature')){
+      $active_jobs = TRUE;
+   }
+
+   if(!$active_jobs){
+
+      $form['chado_feature_accession_prefix'] = array (
+         '#title'       => t('Accession Prefix'),
+         '#type'        => t('textfield'),
+         '#description' => t("Accession numbers for features consist of the ".
+            "chado feature_id and a site specific prefix.  Set the prefix that ".
+            "will be incorporated in front of each feature_id to form a unique ".
+            "accession number for this site."),
+         '#required'    => TRUE,
+         '#default_value' => variable_get('chado_feature_accession_prefix','ID'),
+      );
+
+      $form['chado_feature_types'] = array(
+         '#title'       => t('Feature Types'),
+         '#type'        => 'textarea',
+         '#description' => t('Enter the names of the sequence types that the ".
+            "site will support with independent pages.  Pages for these data ".
+            "types will be built automatically for features that exist in the ".
+            "chado database.  The names listed here should be spearated by ".
+            "spaces or entered separately on new lines. The names must match ".
+            "exactly (spelling and case) with terms in the sequence ontology'),
+         '#required'    => TRUE,
+         '#default_value' => variable_get('chado_feature_types','EST contig'),
+      );
+
+      $form['browser'] = array(
+         '#type' => 'fieldset',
+         '#title' => t('Feature Browser')
+      );
+      $allowedoptions ['show_feature_browser'] = "Show the feature browser on the organism page. The Browser loads when page loads. This may be slow for large sites.";
+      $allowedoptions ['hide_feature_browser'] = "Hide the feature browser on the organism page. Disables the feature browser completely.";
+//      $allowedoptions ['allow_feature_browser'] = "Allow loading of the feature browsing through AJAX. For large sites the initial page load will be quick with the feature browser loading afterwards.";
+
+      $form['browser']['browse_features'] = array(
+         '#title' => 'Feature Browser on Organism Page',
+         '#description' => 'A feature browser can be added to an organism page to allow users to quickly '. 
+            'access a feature.  This will most likely not be the ideal mechanism for accessing feature '.
+            'information, especially for large sites, but it will alow users exploring the site (such '.
+            'as students) to better understand the data types available on the site.',
+         '#type' => 'radios',
+         '#options' => $allowedoptions,
+         '#default_value'=>variable_get('tripal_feature_browse_setting',
+            array('show_feature_browser')),
+      );
+      $form['browser']['set_browse_button'] = array(
+         '#type' => 'submit',
+         '#value' => t('Set Browser'),
+         '#weight' => 2,
+      );
+
+      get_tripal_feature_admin_form_sync_set($form);
+      get_tripal_feature_admin_form_taxonomy_set($form);
+      get_tripal_feature_admin_form_reindex_set($form);
+      get_tripal_feature_admin_form_cleanup_set($form);
+   } else {
+      $form['notice'] = array(
+         '#type' => 'fieldset',
+         '#title' => t('Feature Management Temporarily Unavailable')
+      );
+      $form['notice']['message'] = array(
+         '#value' => t('Currently, feature management jobs are waiting or ".
+            "are running. Managemment features have been hidden until these ".
+            "jobs complete.  Please check back later once these jobs have ".
+            "finished.  You can view the status of pending jobs in the Tripal ".
+            "jobs page.'),
+      );
+   }
+   return system_settings_form($form);
+}
+
+/************************************************************************
+ *
+ */
+function tripal_feature_admin_validate($form, &$form_state) {
+   global $user;  // we need access to the user info
+   $job_args = array();
+
+   // if the user wants to sync up the chado features then
+   // add the job to the management queue
+   if ($form_state['values']['op'] == t('Sync all Features')) {
+      tripal_add_job('Sync all features','tripal_feature',
+         'tripal_feature_sync_features',$job_args,$user->uid);
+   }
+
+   if ($form_state['values']['op'] == t('Set/Reset Taxonomy for all feature nodes')) {
+      tripal_add_job('Set all feature taxonomy','tripal_feature',
+         'tripal_features_set_taxonomy',$job_args,$user->uid);
+   }
+
+   if ($form_state['values']['op'] == t('Reindex all feature nodes')) {
+      tripal_add_job('Reindex all features','tripal_feature',
+         'tripal_features_reindex',$job_args,$user->uid);
+   }
+
+   if ($form_state['values']['op'] == t('Clean up orphaned features')) {
+      tripal_add_job('Cleanup orphaned features','tripal_feature',
+         'tripal_features_cleanup',$job_args,$user->uid);
+   }
+
+   if ($form_state['values']['op'] == t('Set Browser')) {
+      variable_set('tripal_feature_browse_setting',$form_state['values']['browse_features']);
+   }
+
+
+}
+/************************************************************************
+ *
+ */
+function get_tripal_feature_admin_form_cleanup_set(&$form) {
+   $form['cleanup'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Clean Up')
+   );
+   $form['cleanup']['description'] = array(
+       '#type' => 'item',
+       '#value' => t("With Drupal and chado residing in different databases ".
+          "it is possible that nodes in Drupal and features in Chado become ".
+          "\"orphaned\".  This can occur if a feature node in Drupal is ".
+          "deleted but the corresponding chado feature is not and/or vice ".
+          "versa.  The Cleanup function will also remove nodes for features ".
+          "that are not in the list of allowed feature types as specified ".
+          "above.  This is helpful when a feature type needs to be ".
+          "removed but was previously present as Drupal nodes. ".
+          "Click the button below to resolve these discrepancies."),
+       '#weight' => 1,
+   );
+   $form['cleanup']['button'] = array(
+      '#type' => 'submit',
+      '#value' => t('Clean up orphaned features'),
+      '#weight' => 2,
+   );
+}
+/************************************************************************
+ *
+ */
+function get_tripal_feature_admin_form_reindex_set(&$form) {
+   $form['reindex'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Reindex')
+   );
+   $form['reindex']['description'] = array(
+       '#type' => 'item',
+       '#value' => t("Reindexing of nodes is important when content for nodes ".
+          "is updated external to drupal, such as external uploads to chado. ".
+          "Features need to be reindexed to ensure that updates to features ".
+          "are searchable. Depending on the number of features this may take ".
+          "quite a while. Click the button below to begin reindexing of ".
+          "features."),
+       '#weight' => 1,
+   );
+   $form['reindex']['button'] = array(
+      '#type' => 'submit',
+      '#value' => t('Reindex all feature nodes'),
+      '#weight' => 2,
+   );
+}
+/************************************************************************
+ *
+ */
+function get_tripal_feature_admin_form_taxonomy_set (&$form) {
+
+
+   $form['taxonomy'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Set Taxonomy')
+   );
+
+   $form['taxonomy']['description'] = array(
+       '#type' => 'item',
+       '#value' => t("Drupal allows for assignment of \"taxonomy\" or ".
+          "catagorical terms to nodes. These terms allow for advanced ".
+          "filtering during searching."),
+       '#weight' => 1,
+   );
+   $tax_options = array (
+      'organism' => t('Organism name'),
+      'feature_type'  => t('Feature Type (e.g. EST, mRNA, etc.)'),
+      'analysis' => t('Analysis Name'),
+      'library'  => t('Library Name'),
+   );
+   $form['taxonomy']['tax_classes'] = array (
+     '#title'       => t('Available Taxonomic Classes'),
+     '#type'        => t('checkboxes'),
+     '#description' => t("Please select the class of terms to assign to ".
+        "chado features"),
+     '#required'    => FALSE,
+     '#prefix'      => '<div id="taxclass_boxes">',
+     '#suffix'      => '</div>',
+     '#options'     => $tax_options,
+     '#weight'      => 2,
+     '#default_value' => variable_get('tax_classes',''),
+   );
+   $form['taxonomy']['button'] = array(
+      '#type' => 'submit',
+      '#value' => t('Set/Reset Taxonomy for all feature nodes'),
+      '#weight' => 3,
+   );
+
+}
+/************************************************************************
+ *
+ */
+function get_tripal_feature_admin_form_sync_set (&$form) {
+
+  
+   // get the list of organisms which will be synced.
+   $feature_sql = "SELECT * FROM {Feature} WHERE uniquename = '%s' and organism_id = %d";
+   $previous_db = db_set_active('chado');
+   $feature = db_fetch_object(db_query($feature_sql,$node->title,$node->organism_id));
+   db_set_active($previous_db);
+
+   // define the fieldsets
+   $form['sync'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Sync Features')
+   );
+
+   $form['sync']['description'] = array(
+      '#type' => 'item',
+      '#value' => t("Click the 'Sync all Features' button to create Drupal ".
+         "content for features in chado. Only features of the types listed ".
+         "above in the Feature Types box will be synced. Depending on the ".
+         "number of features in the chado database this may take a long ".
+         "time to complete. "),
+      '#weight' => 1,
+   );
+
+   $orgs = tripal_organism_get_synced();   
+   $org_list = '';
+   foreach($orgs as $org){
+      $org_list .= "$org->genus $org->species, ";
+   }
+   $form['sync']['description2'] = array(
+      '#type' => 'item',
+      '#value' => "Only features for the following organisms will be synced: ".
+         " $org_list",
+      '#weight' => 1,
+   );
+
+   $form['sync']['button'] = array(
+      '#type' => 'submit',
+      '#value' => t('Sync all Features'),
+      '#weight' => 3,
+   );
+
+}
+/************************************************************************
+ * Display help and module information
+ * @param path which path of the site we're displaying help
+ * @param arg array that holds the current path as would be returned from arg() function
+ * @return help text for the path
+ */
+function tripal_feature_help($path, $arg) {
+   $output = '';
+   switch ($path) {
+      case "admin/help#tripal_feature":
+         $output='<p>'.t("Displays links to nodes created on this date").'</p>';
+         break;
+   }
+   return $output;
+}
+
+/************************************************************************
+ *  Provide information to drupal about the node types that we're creating
+ *  in this module
+ */
+function tripal_feature_node_info() {
+   $nodes = array();
+
+   $nodes['chado_feature'] = array(
+      'name' => t('Feature'),
+      'module' => 'chado_feature',
+      'description' => t('A feature from the chado database'),
+      'has_title' => FALSE,
+      'title_label' => t('Feature'),
+      'has_body' => FALSE,
+      'body_label' => t('Feature Description'),
+      'locked' => TRUE
+   );
+   return $nodes;
+}
+
+/************************************************************************
+ *  Set the permission types that the chado module uses.  Essentially we
+ *  want permissionis that protect creation, editing and deleting of chado
+ *  data objects
+ */
+function tripal_feature_perm(){
+   return array(
+      'access chado_feature content',
+      'create chado_feature content',
+      'delete chado_feature content',
+      'edit chado_feature content',
+   );
+}
+
+/************************************************************************
+ *  Set the permission types that the module uses.
+ */
+function chado_feature_access($op, $node, $account) {
+   if ($op == 'create') {
+      return user_access('create chado_feature content', $account);
+   }
+
+   if ($op == 'update') {
+      if (user_access('edit chado_feature content', $account)) {
+         return TRUE;
+      }
+   }
+   if ($op == 'delete') {
+      if (user_access('delete chado_feature content', $account)) {
+         return TRUE;
+      }
+   }
+   if ($op == 'view') {
+      if (user_access('access chado_feature content', $account)) {
+         return TRUE;
+      }
+   }
+   return FALSE;
+}
+/************************************************************************
+ *  Menu items are automatically added for the new node types created
+ *  by this module to the 'Create Content' Navigation menu item.  This function
+ *  adds more menu items needed for this module.
+ */
+function tripal_feature_menu() {
+   $items = array();
+
+   // the administative settings menu
+   $items['admin/tripal/tripal_feature'] = array(
+     'title' => 'Features',
+     'description' => 'Settings for Chado Features',
+     'page callback' => 'drupal_get_form',
+     'page arguments' => array('tripal_feature_admin'),
+     'access arguments' => array('administer site configuration'),
+     'type' => MENU_NORMAL_ITEM,
+   );
+
+   $items['admin/settings/tripal/tripal_feature/load'] = array(
+     'title' => 'Bulk Load',
+     'description' => 'Upload Data into Chado & Drupal',
+     'page callback' => 'tripal_feature_bulkload',
+     'access arguments' => array('administer site configuration'),
+     'type' => MENU_NORMAL_ITEM,
+   );
+
+   return $items;
+}
+
+/************************************************************************
+ *  When a new chado_feature node is created we also need to add information
+ *  to our chado_feature table.  This function is called on insert of a new node
+ *  of type 'chado_feature' and inserts the necessary information.
+ */
+function chado_feature_insert($node){
+   // remove spaces, newlines from residues
+   $residues = preg_replace("/[\n\r\s]/","",$node->residues);
+
+   // If this feature already exists then don't recreate it in chado
+   // TODO: the unique index in chado for this also includes the type_id. If the site
+   // ever needs to have the same feature name for different types then this will break.
+   $feature_sql = "SELECT * FROM {Feature} WHERE uniquename = '%s' and organism_id = %d";
+   $previous_db = db_set_active('chado');
+   $feature = db_fetch_object(db_query($feature_sql,$node->title,$node->organism_id));
+   db_set_active($previous_db);
+
+   // if the feature doesn't exist then let's create it in chado.
+   if(!$feature){
+      $sql = "INSERT INTO {feature} (organism_id, name, uniquename, residues, seqlen,".
+             "    is_obsolete, type_id)".
+             " VALUES(%d,'%s','%s','%s',%d, %s, ".
+             "   (SELECT cvterm_id ".
+             "    FROM {CVTerm} CVT ".
+             "    INNER JOIN CV ON CVT.cv_id = CV.cv_id ".
+             "    WHERE CV.name = 'sequence' and CVT.name = '%s'))";
+      $obsolete = 'FALSE';
+      if($node->is_obsolete){
+         $obsolete = 'TRUE';
+      }
+
+      // use chado database
+      $previous_db = db_set_active('chado');
+      db_query($sql,$node->organism_id,$node->title,$node->title,
+      $residues,strlen($residues),$obsolete,$node->feature_type);
+
+      // now that we've added the feature, get the feature id for this feature
+      $feature = db_fetch_object(db_query($feature_sql,$node->title,$node->organism_id));
+
+      // now use drupal database
+      db_set_active($previous_db);
+   }
+
+   // add the genbank accession and synonyms
+   chado_feature_add_synonyms($node->synonyms,$feature->feature_id);
+
+   // make sure the entry for this feature doesn't already exist in the chado_feature table
+   // if it doesn't exist then we want to add it.
+   $node_check_sql = "SELECT * FROM {chado_feature} ".
+                     "WHERE feature_id = '%s'";
+   $node_check = db_fetch_object(db_query($node_check_sql,$feature->feature_id));
+   if(!$node_check){
+      // next add the item to the drupal table
+      $sql = "INSERT INTO {chado_feature} (nid, vid, feature_id, sync_date) ".
+             "VALUES (%d, %d, %d, " . time() . ")";
+      db_query($sql,$node->nid,$node->vid,$feature->feature_id);
+   }
+}
+/************************************************************************
+ */
+function chado_feature_delete($node){
+   // get feature_id so we can remove it from chado database
+   $sql_drupal = "SELECT feature_id ".
+                 "FROM {chado_feature} ".
+                 "WHERE nid = %d AND vid = %d";
+   $feature_id = db_result(db_query($sql_drupal, $node->nid, $node->vid));
+
+   // remove the drupal content  
+   $sql_del = "DELETE FROM {chado_feature} ".
+              "WHERE nid = %d ".
+              "AND vid = %d";
+   db_query($sql_del, $node->nid, $node->vid);
+   $sql_del = "DELETE FROM {node} ".
+              "WHERE nid = %d ".
+              "AND vid = %d";
+   db_query($sql_del, $node->nid, $node->vid);
+   $sql_del = "DELETE FROM {node_revisions} ".
+              "WHERE nid = %d ".
+              "AND vid = %d";
+   db_query($sql_del, $node->nid, $node->vid);
+
+
+   // Remove data from feature tables of chado database.  This will
+   // cause a cascade delete and remove all data in referencing tables
+   // for this feature
+   $previous_db = db_set_active('chado');
+   db_query("DELETE FROM {feature} WHERE feature_id = %d", $feature_id);
+   db_set_active($previous_db);
+   
+   drupal_set_message("The feature and all associated data were removed from ".
+      "chado");
+
+}
+/************************************************************************
+ */
+function chado_feature_update($node){
+   if($node->revision){
+      // TODO -- decide what to do about revisions
+   } else {
+      // get the feature for this node:
+      $sql = 'SELECT feature_id FROM {chado_feature} WHERE vid = %d';
+      $feature = db_fetch_object(db_query($sql, $node->vid));
+
+      // remove spaces, newlines from residues
+      $residues = preg_replace("/[\n\r\s]/","",$node->residues);
+
+      $sql = "UPDATE {feature} ".
+             " SET residues = '%s', ".
+             "   name = '%s', ".
+             "   uniquename = '%s', ".
+             "   seqlen = %d, ".
+             "   organism_id = %d, ".
+             "   is_obsolete = %s, ".
+             "   type_id = (SELECT cvterm_id ".
+             "              FROM {CVTerm} CVT ".
+             "              INNER JOIN CV ON CVT.cv_id = CV.cv_id ".
+             "              WHERE CV.name = 'sequence' and CVT.name = '%s') ".
+             "WHERE feature_id = %d ";
+      $obsolete = 'FALSE';
+      if($node->is_obsolete){
+         $obsolete = 'TRUE';
+      }
+      $previous_db = db_set_active('chado');  // use chado database
+      db_query($sql,$residues,$node->title,$node->title,
+      strlen($residues),$node->organism_id,$obsolete,$node->feature_type,
+      $feature->feature_id);
+      db_set_active($previous_db);  // now use drupal database
+
+      // add the genbank accession & synonyms
+      // chado_feature_add_gbaccession($node->gbaccession,$feature->feature_id);
+      chado_feature_add_synonyms($node->synonyms,$feature->feature_id);
+   }
+}
+/************************************************************************
+ *
+ */
+function chado_feature_add_synonyms($synonyms,$feature_id){
+
+   // make sure we only have a single space between each synonym
+   $synonyms = preg_replace("/[\s\n\r]+/"," ",$synonyms);
+   // split the synonyms into an array based on a space as the delimieter
+   $syn_array = array();
+   $syn_array = explode(" ",$synonyms);
+
+   // use the chado database
+   $previous_db = db_set_active('chado');
+
+   // remove any old synonyms
+   $feature_syn_dsql = "DELETE FROM {feature_synonym} WHERE feature_id = %d";
+   if(!db_query($feature_syn_dsql,$feature_id)){
+      $error .= "Could not remove synonyms from feature. ";
+   }
+
+   // return if we don't have any synonmys to add
+   if(!$synonyms){
+      db_set_active($previous_db);
+      return;
+   }
+   // iterate through each synonym and add it to the database
+   foreach($syn_array as $syn){
+      // skip this item if it's empty
+      if(!$syn){ break; }
+
+      // check to see if we have this accession number already in the database
+      // if so then don't add it again. it messes up drupal if the insert fails.
+      // It is possible for the accession number to be present and not the feature
+      $synonym_sql = "SELECT synonym_id FROM {synonym} ".
+                     "WHERE name = '%s'";
+      $synonym = db_fetch_object(db_query($synonym_sql,$syn));
+      if(!$synonym){
+         $synonym_isql = "INSERT INTO {synonym} (name,synonym_sgml,type_id) ".
+                         "VALUES ('%s','%s', ".
+                         "   (SELECT cvterm_id ".
+                         "    FROM {CVTerm} CVT ".
+                         "    INNER JOIN CV ON CVT.cv_id = CV.cv_id ".
+                         "    WHERE CV.name = 'feature_property' and CVT.name = 'synonym'))";
+         if(!db_query($synonym_isql,$syn,$syn)){
+            $error .= "Could not add synonym. ";
+         }
+         // now get the synonym we just added
+         $synonym_sql = "SELECT synonym_id FROM {synonym} ".
+                        "WHERE name = '%s'";
+         $synonym = db_fetch_object(db_query($synonym_sql,$syn));
+      }
+
+      // now add in our new sysnonym
+      $feature_syn_isql = "INSERT INTO {feature_synonym} (synonym_id,feature_id,pub_id) ".
+                          "VALUES (%d,%d,1)";
+      if(!db_query($feature_syn_isql,$synonym->synonym_id,$feature_id)){
+         $error .= "Could not add synonyms to feature. ";
+      }
+   }
+
+   // return to the drupal database
+   db_set_active($previous_db);
+   return $error;
+
+}
+/************************************************************************
+ *
+ */
+function chado_feature_add_gbaccession($accession,$feature_id){
+
+   // use chado database
+   $previous_db = db_set_active('chado');
+
+   // remove any old accession from genbank dbEST
+   $fdbxref_dsql = "DELETE FROM {feature_dbxref} ".
+                   "WHERE feature_id = %d and dbxref_id IN ".
+                   "   (SELECT DBX.dbxref_id FROM {dbxref} DBX ".
+                   "    INNER JOIN DB  ON DB.db_id = DBX.db_id ".
+                   "    INNER JOIN feature_dbxref FDBX ON DBX.dbxref_id = FDBX.dbxref_id ".
+                   "    WHERE DB.name = 'DB:Genbank' and FDBX.feature_id = %d)";
+   if(!db_query($fdbxref_dsql,$feature_id,$feature_id)){
+      $error .= "Could not remove accession from feature. ";
+   }
+
+   // if we don't have an accession number to add then just return
+   if(!$accession){
+      db_set_active($previous_db);
+      return;
+   }
+   // get the db_id
+   $db_sql = "SELECT db_id FROM {DB} ".
+             "WHERE name = 'DB:Genbank_est'";
+   $db = db_fetch_object(db_query($db_sql));
+
+   // check to see if we have this accession number already in the database
+   // if so then don't add it again. it messes up drupal if the insert fails.
+   // It is possible for the accession number to be present and not the feature
+   $dbxref_sql = "SELECT dbxref_id FROM {dbxref} ".
+                 "WHERE db_id = %d and accession = '%s'";
+   $dbxref = db_fetch_object(db_query($dbxref_sql,$db->db_id,$accession));
+   if(!$dbxref){
+      // add the accession number
+      $dbxref_isql = "INSERT INTO {dbxref} (db_id,accession) ".
+                     "  VALUES (%d, '%s') ";
+      if(!db_query($dbxref_isql,$db->db_id,$accession)){
+         $error .= 'Could not add accession as a database reference ';
+      }
+      // get the dbxref_id for the just added accession number
+      $dbxref_sql = "SELECT dbxref_id FROM {dbxref} ".
+                    "WHERE db_id = %d and accession = '%s'";
+      $dbxref = db_fetch_object(db_query($dbxref_sql,$db->db_id,$accession));
+   }
+
+
+   // associate the accession number with the feature
+   $feature_dbxref_isql = "INSERT INTO {feature_dbxref} (feature_id,dbxref_id) ".
+                          "  VALUES (%d, %d) ";
+   if(!db_query($feature_dbxref_isql,$feature_id,$dbxref->dbxref_id)){
+      $error .= 'Could not add feature database reference. ';
+   }
+
+   db_set_active($previous_db);
+   return $error;
+}
+
+/************************************************************************
+ *
+ */
+function chado_feature_form ($node,$param){
+   $type = node_get_types('type', $node);
+   $form = array();
+   $feature = $node->feature;
+   $synonyms = $node->synonyms;
+   $analyses = $node->analyses;
+   $references = $node->references;
+
+   // We need to pass above variables for preview to show
+   $form['feature'] = array(
+      '#type' => 'value',
+      '#value' => $feature
+   );
+   // This field is read when previewing a node
+   $form['synonyms'] = array(
+      '#type' => 'value',
+      '#value' => $synonyms
+   );
+   // This field is read when previewing a node
+   $form['analyses'] = array(
+      '#type' => 'value',
+      '#value' => $analyses
+   );
+   // This field is read when previewing a node
+   $form['references'] = array(
+      '#type' => 'value',
+      '#value' => $references
+   );
+
+   // keep track of the feature id if we have one.  If we do have one then
+   // this would indicate an update as opposed to an insert.
+   $form['feature_id'] = array(
+      '#type' => 'value',
+      '#value' => $feature->feature_id,
+   );
+
+   $form['title']= array(
+      '#type' => 'textfield',
+      '#title' => t('Unique Feature Name'),
+      '#required' => TRUE,
+      '#default_value' => $feature->featurename,
+      '#description' => t('Enter a unique name for this feature'),
+      '#weight' => 1,
+      '#maxlength' => 255
+   );
+
+   // get the list of supported feature types
+   $ftypes = array();
+   $ftypes[''] = '';
+   $supported_ftypes = split("[ \n]",variable_get('chado_feature_types','EST contig'));
+   foreach($supported_ftypes as $ftype){
+      $ftypes["$ftype"] = $ftype;
+   }
+
+   $form['feature_type'] = array (
+     '#title'       => t('Feature Type'),
+     '#type'        => t('select'),
+     '#description' => t("Choose the feature type."),
+     '#required'    => TRUE,
+     '#default_value' => $feature->cvname,
+     '#options'     => $ftypes,
+     '#weight'      => 2
+   );
+   // get the list of organisms
+   $sql = "SELECT * FROM {Organism} ORDER BY genus, species";
+   $previous_db = db_set_active('chado');  // use chado database
+   $org_rset = db_query($sql);
+   db_set_active($previous_db);  // now use drupal database
+
+   //
+   $organisms = array();
+   $organisms[''] = '';
+   while($organism = db_fetch_object($org_rset)){
+      $organisms[$organism->organism_id] = "$organism->genus $organism->species ($organism->common_name)";
+   }
+
+   $form['organism_id'] = array (
+     '#title'       => t('Organism'),
+     '#type'        => t('select'),
+     '#description' => t("Choose the organism with which this feature is associated "),
+     '#required'    => TRUE,
+     '#default_value' => $feature->organism_id,
+     '#options'     => $organisms,
+     '#weight'      => 3,
+   );
+
+   // Get synonyms
+   if ($synonyms) {
+      if (is_array($synonyms)) {
+         foreach ($synonyms as $synonym){
+            $syn_text .= "$synonym->name\n";
+         }
+      } else {
+         $syn_text = $synonyms;
+      }
+   }
+   $form['synonyms']= array(
+      '#type' => 'textarea',
+      '#title' => t('Synonyms'),
+      '#required' => FALSE,
+      '#default_value' => $syn_text,
+      '#description' => t('Enter alternate names (synonmys) for this feature to help in searching and identification. You may enter as many alternate names as needed separated by spaces or on different lines.'),
+      '#weight' => 5,
+   );
+
+   $form['residues']= array(
+      '#type' => 'textarea',
+      '#title' => t('Residues'),
+      '#required' => FALSE,
+      '#default_value' => $feature->residues,
+      '#description' => t('Enter the nucelotide sequences for this feature'),
+      '#weight' => 6
+   );
+
+   $checked = '';
+   if($feature->is_obsolete == 't'){
+      $checked = '1';
+   }
+   $form['is_obsolete']= array(
+      '#type' => 'checkbox',
+      '#title' => t('Is Obsolete'),
+      '#required' => FALSE,
+      '#default_value' => $checked,
+      '#description' => t('Check this box if this sequence should be retired and no longer included in further analysis.'),
+      '#weight' => 8
+   );
+   return $form;
+}
+/************************************************************************
+ *
+ */
+function chado_feature_validate($node){
+   $result = 0;
+
+   // if this is an update, we want to make sure that a different feature for
+   // the organism doesn't already have this uniquename. We don't want to give
+   // two sequences the same uniquename
+   if($node->feature_id){
+      $sql = "SELECT * FROM {Feature} WHERE uniquename = '%s' ".
+             "  AND organism_id = %d AND NOT feature_id = %d";
+      $previous_db = db_set_active('chado');
+      $result = db_fetch_object(db_query($sql, $node->title,$node->organism_id,$node->feature_id));
+      db_set_active($previous_db);
+      if($result){
+         form_set_error('title',t("Feature update cannot proceed. The feature name '$node->title' is not unique for this organism. Please provide a unique name for this feature. "));
+      }
+   }
+
+   // if this is an insert then we just need to make sure this name doesn't
+   // already exist for this organism if it does then we need to throw an error
+   else {
+      $sql = "SELECT * FROM {Feature} WHERE uniquename = '%s' AND organism_id = %d";
+      $previous_db = db_set_active('chado');
+      $result = db_fetch_object(db_query($sql, $node->title,$node->organism_id));
+      db_set_active($previous_db);
+      if($result){
+         form_set_error('title',t("Feature insert cannot proceed. The feature name '$node->title' already exists for this organism. Please provide a unique name for this feature. "));
+      }
+   }
+
+   // we want to remove all characters except IUPAC nucleotide characters from the
+   // the residues. however, residues are not required so if blank then we'll skip
+   // this step
+   if($node->residues){
+      $residues = preg_replace("/[^\w]/",'',$node->residues);
+      if(!preg_match("/^[ACTGURYMKSWBDHVN]+$/i",$residues)){
+         form_set_error('residues',t("The residues in feature $node->title contains more than the nucleotide IUPAC characters. Only the following characters are allowed: A,C,T,G,U,R,Y,M,K,S,W,B,D,H,V,N: '" . $residues ."'"));
+      }
+   }
+
+   // we don't allow a genbank accession number for a contig
+   if($node->feature_type == 'contig' and $node->gbaccession){
+      form_set_error('gbaccession',t("Contigs cannot have a genbank accession number.  Please change the feature type or remove the accession number"));
+   }
+
+
+
+}
+/************************************************************************
+ *  When a node is requested by the user this function is called to allow us
+ *  to add auxiliary data to the node object.
+ */
+function chado_feature_load($node){
+   // get the feature_id for this node:
+   $sql = 'SELECT feature_id FROM {chado_feature} WHERE vid = %d';
+   $map = db_fetch_object(db_query($sql, $node->vid));
+
+   $previous_db = db_set_active('chado');  // use chado database
+
+   // get information about this organism and add it to the items in this node
+   $sql = "SELECT F.feature_id, F.name as featurename, F.uniquename, ".
+          "F.residues, F.seqlen, O.genus, O.species, O.common_name, ".
+          "  CVT.name as cvname, O.organism_id, F.type_id, F.is_obsolete  ".
+          "FROM {Feature} F ".
+          "  INNER JOIN Organism O ON F.organism_id = O.organism_id ".
+          "  INNER JOIN CVterm CVT ON F.type_id = CVT.cvterm_id ".
+          "WHERE F.feature_id = %d";
+   $feature = db_fetch_object(db_query($sql,$map->feature_id));
+   $additions->feature = $feature;
+   $additions->seqlen = $feature->seqlen;
+   
+   // get the feature synonyms
+   $sql = "SELECT S.name ".
+          "FROM {Feature_Synonym} FS ".
+          "  INNER JOIN Synonym S ".
+          "    ON FS.synonym_id = S.Synonym_id ".
+          "WHERE FS.feature_id = %d";
+   $results = db_query($sql,$map->feature_id);
+   $synonyms = array();
+   $i=0;
+   while($synonym = db_fetch_object($results)){
+      $synonyms[$i++] = $synonym;
+   }
+   $additions->synonyms = $synonyms;
+
+   // get feature references in external databases
+   $sql = "SELECT F.uniquename,F.Feature_id,DBX.accession,DB.description as dbdesc, ".
+          "   DB.db_id, DB.name as db_name, DB.urlprefix ".
+          "FROM {Feature} F ".
+          "  INNER JOIN Feature_dbxref FDBX on F.feature_id = FDBX.feature_id ".
+          "  INNER JOIN Dbxref DBX on DBX.dbxref_id = FDBX.dbxref_id ".
+          "  INNER JOIN DB on DB.db_id = DBX.db_id ".
+          "WHERE F.feature_id = %d";
+   $results = db_query($sql,$map->feature_id);
+   $references = array();
+   $i=0;
+   while($accession = db_fetch_object($results)){
+      $references[$i++] = $accession;
+      // we want to specifically pull out the genbank id
+      if(preg_match("/Genbank_est/",$accession->db_name)){
+         $additions->gbaccession = $accession;
+      }
+   }
+   $additions->references = $references;
+   db_set_active($previous_db);  // now use drupal database
+
+   // get organism node nid
+   $sql = "SELECT nid FROM {chado_organism} WHERE organism_id = %d";
+   $org_nid = db_result(db_query($sql, $additions->feature->organism_id));
+   $additions->org_nid = $org_nid;
+   return $additions;
+}
+
+/************************************************************************
+ *  This function customizes the view of the chado_feature node.  It allows
+ *  us to generate the markup.
+ */
+function chado_feature_view ($node, $teaser = FALSE, $page = FALSE) {
+
+   if (!$teaser) {
+      // use drupal's default node view:
+      $node = node_prepare($node, $teaser);
+            
+      // if we're building the node for searching then
+      // we want to handle this within the module and
+      // not allow theme customization.  We don't want to
+      // index all items (such as DNA sequence).
+      if($node->build_mode == NODE_BUILD_SEARCH_INDEX){
+         $node->content['index_version'] = array(
+            '#value' => theme('tripal_feature_search_index',$node),
+            '#weight' => 1,
+         );
+      }
+      else if($node->build_mode == NODE_BUILD_SEARCH_RESULT){
+         $node->content['index_version'] = array(
+            '#value' => theme('tripal_feature_search_results',$node),
+            '#weight' => 1,
+         );
+      }
+      else {
+         // do nothing here, let the theme derived template handle display
+      }
+   }
+
+   return $node;
+}
+
+/*******************************************************************************
+ * Display feature information for associated organisms. This function also
+ * provides contents for indexing
+ */
+function tripal_feature_nodeapi(&$node, $op, $teaser, $page) {
+
+   switch ($op) {
+      // Note that this function only adds feature view to an organism node.
+      // The view of a feature node is controled by the theme *.tpl file
+      case 'view':
+         // Set the node types for showing feature information
+         $types_to_show = array('chado_organism', 'chado_library');
+
+         // Abort if this node is not one of the types we should show.
+         if (!in_array($node->type, $types_to_show, TRUE)) {
+            break;
+         }
+
+         // Add feature to the content item if it's not a teaser
+         if (!$teaser) {
+
+            // Show feature browser
+            $node->content['tripal_feature_browser'] = array(
+               '#value' => theme('tripal_feature_browser', $node),
+               '#weight' => 5
+            );
+            $node->content['tripal_feature_org_counts'] = array(
+               '#value' => theme('tripal_feature_counts', $node),
+               '#weight' => 4
+            );
+         }
+   }
+}
+/************************************************************************
+ *  We need to let drupal know about our theme functions and their arguments.
+ *  We create theme functions to allow users of the module to customize the
+ *  look and feel of the output generated in this module
+ */
+function tripal_feature_theme () {
+   return array(
+      'tripal_feature_search_index' => array (
+         'arguments' => array('node'),
+       ),
+      'tripal_feature_search_results' => array (
+         'arguments' => array('node'),
+      ),
+      'tripal_feature_browser' => array (
+         'arguments' => array('node'),
+      ),
+      'tripal_feature_counts' => array (
+         'arguments' => array('node'),
+      )
+   );
+}
+/*******************************************************************************
+ *  create a list of features for the organism and pie chart
+ */
+function theme_tripal_feature_counts($node){
+
+   // get the feature counts.  This is dependent on a materialized view
+   // installed with the organism module
+   $content = '';
+   if ($node->organism_id && $node->type == 'chado_organism') {
+      $sql = "SELECT * FROM {organism_feature_count} ".
+      		 "WHERE organism_id = %d AND NOT feature_type = 'EST_match' ".
+             "ORDER BY num_features desc";
+      $features = array();
+      $previous_db = db_set_active('chado');  // use chado database
+      $results = db_query($sql,$node->organism_id);
+      db_set_active($previous_db);  // now use drupal database
+      $feature = db_fetch_object($results); // retrieve the first result
+      if ($feature) {
+         $content .= "<div class=\"tripal_feature_summary-info-box\"><br>
+                             <div class=\"tripal_expandableBox\">".
+                     "<h3>Feature Summary</h3>".
+                     "</div>";
+         $content .= "<div class=\"tripal_expandableBoxContent\">";
+         $content .= "<table class=\"tripal_table_horz\">";
+         $content .= "  <tr>";
+         $content .= "    <th class=\"dbfieldname\">Type</th>";
+         $content .= "    <th class=\"dbfieldname\">Number</th>";
+         $content .= "  </tr>";
+         do {
+            $content .= "<tr>";
+            $content .= "  <td>$feature->feature_type</td>";
+            $content .= "  <td>". number_format($feature->num_features) . "</td>";
+            $content .= "</tr>";
+         } while($feature = db_fetch_object($results));
+         $content .= "</table>";
+         $content .= "
+            <img class=\"tripal_cv_chart\" id=\"tripal_feature_cv_chart_$node->organism_id\" src=\"\" border=\"0\">
+         ";
+         $content .= "</div></div>";
+      }
+   }
+   return $content;
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_cv_chart($chart_id){
+
+  // The CV module will create the JSON array necessary for buillding a
+  // pie chart using jgChart and Google Charts.  We have to pass to it
+  // a table that contains count information, tell it which column 
+  // contains the cvterm_id and provide a filter for getting the
+  // results we want from the table.
+  $organism_id = preg_replace("/^tripal_feature_cv_chart_(\d+)$/","$1",$chart_id);
+  $options = array(
+     count_mview      => 'organism_feature_count',
+     cvterm_id_column => 'cvterm_id',
+     count_column     => 'num_features',
+     filter           => "CNT.organism_id = $organism_id AND NOT feature_type = 'EST_match' ",
+  );
+  return $options;
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_cv_tree($tree_id){
+
+  // The CV module will create the JSON array necessary for buillding a
+  // pie chart using jgChart and Google Charts.  We have to pass to it
+  // a table that contains count information, tell it which column 
+  // contains the cvterm_id and provide a filter for getting the
+  // results we want from the table.
+  $organism_id = preg_replace("/^tripal_feature_cv_tree_(\d+)$/","$1",$tree_id);
+  $options = array(
+     cv_id            => tripal_cv_get_cv_id('sequence'),
+     count_mview      => 'organism_feature_count',
+     cvterm_id_column => 'cvterm_id',
+     count_column     => 'num_features',
+     filter           => "CNT.organism_id = $organism_id",
+     label            => 'Features',
+  );
+  return $options;
+}
+/*******************************************************************************
+ *  create a simple paged feature browser
+ */
+function theme_tripal_feature_browser($node){
+
+   // don't show the browser if the settings in the admin page is turned off
+   $show_browser = variable_get('tripal_feature_browse_setting',array('show_feature_browser'));
+
+   if(strcmp($show_browser,'show_feature_browser')!=0){
+      return;
+   }
+   if ($node->organism_id && $node->type == 'chado_organism') {
+      # get the list of available sequence ontology terms for which
+      # we will build drupal pages from features in chado.  If a feature
+      # is not one of the specified typse we won't build a node for it.
+      $allowed_types = variable_get('chado_feature_types','EST contig');
+      $allowed_types = preg_replace("/[\s\n\r]+/"," ",$allowed_types);
+      $so_terms = split(' ',$allowed_types);
+      $where_cvt = "";
+      foreach ($so_terms as $term){
+         $where_cvt .= "CVT.name = '$term' OR ";
+      }
+      $where_cvt = substr($where_cvt,0,strlen($where_cvt)-3);  # strip trailing 'OR'
+
+      // get the features for this organism
+      $sql  = "SELECT F.name,F.feature_id,F.uniquename,CVT.name as cvname ".
+              "FROM {feature} F ".
+                 "  INNER JOIN {cvterm} CVT on F.type_id = CVT.cvterm_id ".
+               "WHERE organism_id = $node->organism_id and ($where_cvt) ".
+               "ORDER BY feature_id ASC";
+
+      // the counting SQL
+      $csql  = "SELECT count(*) ".
+               "FROM {feature} F".
+               "  INNER JOIN {cvterm} CVT on F.type_id = CVT.cvterm_id ".
+               "WHERE organism_id = $node->organism_id and ($where_cvt) ".
+               "GROUP BY organism_id ";
+
+      $previous_db = db_set_active('chado');  // use chado database
+      $features = pager_query($sql,10,0,$csql);
+      db_set_active($previous_db);  // now use drupal database
+
+      $content = "<br><div id=\"tripal_feature_box\" class=\"feature-info-box\">";
+      $content .= "<div class=\"tripal_expandableBox\">".
+                  "<h3>Browse Features</h3>".
+                  "</div>";
+      $content .= "<div class=\"tripal_expandableBoxContent\">";
+      $content .= "Below are the features associated with this organism.\n";
+      $content .= "<table class=\"tripal_table_horz\">";
+      $content .= "  <tr>";
+      $content .= "    <th>Feature Name</th>";
+      $content .= "    <th>Type</th>";
+      $content .= "  </tr>";
+
+      // prepare the query that will lookup node ids
+      $sql = "SELECT nid FROM {chado_feature} ".
+              "WHERE feature_id = %d";
+      while($feature = db_fetch_object($features)){
+         $node = db_fetch_object(db_query($sql,$feature->feature_id));
+         if($node){
+            $name= "<a href=\"" . url("node/$node->nid") . "\">$feature->name</a>";
+         } else {
+            $name= "$feature->name";
+         }
+         $content .= "  <tr>";
+         $content .= "    <td>$name</td>";
+         $content .= "    <td>$feature->cvname</td>";
+         $content .= "  </tr>";
+      }
+      $content .= "</table>";
+      $content .= theme('pager');
+      $content .= "</div></div>";
+      return $content;
+   }
+}
+/************************************************************************
+ *  This function is an extension of the chado_feature_view by providing
+ *  the markup for the feature object THAT WILL BE INDEXED.
+ */
+function theme_tripal_feature_search_index ($node) {
+   $feature = $node->feature;
+   $content = '';
+
+   // get the accession prefix
+   $aprefix = variable_get('chado_feature_accession_prefix','ID');
+
+   $content .= "<h1>$feature->uniquename</h1>. ";
+   $content .= "<strong>$aprefix$feature->feature_id.</strong> ";
+   $content .= "$feature->cvname ";
+   $content .= "$feature->common_name ";
+
+   // add the synonyms of this feature to the text for searching
+   $synonyms = $node->synonyms;
+   if(count($synonyms) > 0){
+      foreach ($synonyms as $result){
+         $content .= "$result->name ";
+      }
+   }
+
+   return $content;
+}
+/************************************************************************
+ *  This function is an extension of the chado_feature_view by providing
+ *  the markup for the feature object THAT WILL BE INDEXED.
+ */
+function theme_tripal_feature_search_results ($node) {
+   $feature = $node->feature;
+   $content = '';
+
+   // get the accession prefix
+   $aprefix = variable_get('chado_feature_accession_prefix','ID');
+
+   $content .= "Feature Name: <h1>$feature->uniquename</h1>. ";
+   $content .= "<strong>Accession: $aprefix$feature->feature_id.</strong>";
+   $content .= "Type: $feature->cvname. ";
+   $content .= "Organism: $feature->common_name. ";
+
+   // add the synonyms of this feature to the text for searching
+   $synonyms = $node->synonyms;
+   if(count($synonyms) > 0){
+      $content .= "Synonyms: ";
+      foreach ($synonyms as $result){
+         $content .= "$result->name, ";
+      }
+   }
+
+   return $content;
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_sync_features ($max_sync = 0, $job_id = NULL){
+   $i = 0;
+
+   // get the list of available sequence ontology terms for which
+   // we will build drupal pages from features in chado.  If a feature
+   // is not one of the specified typse we won't build a node for it.
+   $allowed_types = variable_get('chado_feature_types','EST contig');
+   $allowed_types = preg_replace("/[\s\n\r]+/"," ",$allowed_types);
+   $so_terms = split(' ',$allowed_types);
+   $where_cvt = "";
+   foreach ($so_terms as $term){
+      $where_cvt .= "CVT.name = '$term' OR ";
+   }
+   $where_cvt = substr($where_cvt,0,strlen($where_cvt)-3);  # strip trailing 'OR'
+
+   // get the list of organisms that are synced and only include features from
+   // those organisms
+   $orgs = tripal_organism_get_synced();
+   $where_org = "";
+   foreach($orgs as $org){
+      $where_org .= "F.organism_id = $org->organism_id OR ";
+   }
+   $where_org = substr($where_org,0,strlen($where_org)-3);  # strip trailing 'OR'
+
+   // use this SQL statement to get the features that we're going to upload
+   $sql = "SELECT feature_id ".
+          "FROM {FEATURE} F ".
+          "   INNER JOIN Cvterm CVT ON F.type_id = CVT.cvterm_id ".
+          "WHERE ($where_cvt) AND ($where_org) ".
+          "ORDER BY feature_id";
+   // get the list of features
+   $previous_db = db_set_active('chado');  // use chado database
+   $results = db_query($sql);
+
+   db_set_active($previous_db);  // now use drupal database
+
+   // load into ids array
+   $count = 0;
+   $ids = array();
+   while($id = db_fetch_object($results)){
+      $ids[$count] = $id->feature_id;
+      $count++;
+   }
+
+   // make sure our vocabularies are set before proceeding
+   tripal_feature_set_vocabulary();
+
+   // pre-create the SQL statement that will be used to check
+   // if a feature has already been synced.  We skip features
+   // that have been synced
+   $sql = "SELECT * FROM {chado_feature} WHERE feature_id = %d";
+
+   // Iterate through features that need to be synced
+   $interval = intval($count * 0.01);
+   foreach($ids as $feature_id){
+      // update the job status every 1% features
+      if($job_id and $i % $interval == 0){
+         tripal_job_set_progress($job_id,intval(($i/$count)*100));
+      }
+      // if we have a maximum number to sync then stop when we get there
+      // if not then just continue on
+      if($max_sync and $i == $max_sync){
+         return '';
+      }
+      if(!db_fetch_object(db_query($sql,$feature_id))){
+         tripal_feature_sync_feature ($feature_id);
+      }
+      $i++;
+   }
+
+   return '';
+}
+
+/************************************************************************
+ *
+ */
+function tripal_feature_sync_feature ($feature_id){
+
+   global $user;
+   $create_node = 1;   // set to 0 if the node exists and we just sync and not create
+
+   // get the accession prefix
+   $aprefix = variable_get('chado_feature_accession_prefix','ID');
+
+   // if we don't have a feature_id then return
+   if(!$feature_id){
+      drupal_set_message(t("Please provide a feature_id to sync"));
+      return '';
+   }
+
+   // get information about this feature
+   $fsql = "SELECT F.feature_id, F.name, F.uniquename,O.genus, ".
+           "    O.species,CVT.name as cvname,F.residues,F.organism_id ".
+           "FROM {FEATURE} F ".
+           "  INNER JOIN Cvterm CVT ON F.type_id = CVT.cvterm_id ".
+           "  INNER JOIN Organism O ON F.organism_id = O.organism_ID ".
+           "WHERE F.feature_id = %d";
+   $previous_db = db_set_active('chado');  // use chado database
+   $feature = db_fetch_object(db_query($fsql,$feature_id));
+   db_set_active($previous_db);  // now use drupal database
+
+   // check to make sure that we don't have any nodes with this feature name as a title
+   // but without a corresponding entry in the chado_feature table if so then we want to
+   // clean up that node.  (If a node is found we don't know if it belongs to our feature or
+   // not since features can have the same name/title.)
+   $tsql =  "SELECT * FROM {node} N ".
+            "WHERE title = '%s'";
+   $cnsql = "SELECT * FROM {chado_feature} ".
+            "WHERE nid = %d";
+   $nodes = db_query($tsql,$feature->name);
+   // cycle through all nodes that may have this title
+   while($node = db_fetch_object($nodes)){
+      $feature_nid = db_fetch_object(db_query($cnsql,$node->nid));
+      if(!$feature_nid){
+         drupal_set_message(t("$feature_id: A node is present but the chado_feature entry is missing... correcting"));
+         node_delete($node->nid);
+      }
+   }
+
+   // check if this feature already exists in the chado_feature table.
+   // if we have a chado feature, we want to check to see if we have a node
+   $cfsql = "SELECT * FROM {chado_feature} ".
+            "WHERE feature_id = %d";
+   $nsql =  "SELECT * FROM {node} ".
+            "WHERE nid = %d";
+   $chado_feature = db_fetch_object(db_query($cfsql,$feature->feature_id));
+   if($chado_feature){
+      drupal_set_message(t("$feature_id: A chado_feature entry exists"));
+      $node = db_fetch_object(db_query($nsql,$chado_feature->nid));
+      if(!$node){
+         // if we have a chado_feature but not a node then we have a problem and
+         // need to cleanup
+         drupal_set_message(t("$feature_id: The node is missing, but has a chado_feature entry... correcting"));
+         $df_sql = "DELETE FROM {chado_feature} WHERE feature_id = %d";
+         db_query($df_sql,$feature_id);
+      } else {
+         drupal_set_message(t("$feature_id: A corresponding node exists"));
+         $create_node = 0;
+      }
+   }
+
+   // if we've encountered an error then just return.
+   if($error_msg = db_error()){
+      //print "$error_msg\n";
+      return '';
+   }
+
+   // if a drupal node does not exist for this feature then we want to
+   // create one.  Note that the node_save call in this block
+   // will call the hook_submit function which
+   if($create_node){
+      drupal_set_message(t("$feature_id: Creating node $feature->name"));
+      $new_node = new stdClass();
+      $new_node->type = 'chado_feature';
+      $new_node->uid = $user->uid;
+      $new_node->title = "$feature->name";
+      $new_node->feature_id = $feature->feature_id;
+      $new_node->residues = $feature->residues;
+      $new_node->organism_id = $feature->organism_id;
+      $new_node->feature_type = $feature->cvname;
+
+      // validate the node and if okay then submit
+      node_validate($new_node);
+      if ($errors = form_get_errors()) {
+         foreach($errors as $key => $msg){
+            drupal_set_message($msg);
+         }
+         return $errors;
+      } else {
+         $node = node_submit($new_node);
+         node_save($node);
+      }
+
+   }
+   else {
+      $node = $chado_feature;
+   }
+
+
+   // set the taxonomy for this node
+   drupal_set_message(t("$feature_id ($node->nid): setting taxonomy"));
+   tripal_feature_set_taxonomy($node,$feature_id);
+
+   // reindex the node
+   drupal_set_message(t("$feature_id( $node->nid): indexing"));
+   tripal_feature_index_feature ($feature_id,$node->nid);
+
+   // remove any URL alias that may already exist and recreate
+   drupal_set_message(t("$feature_id ($node->nid): setting URL alias"));
+   db_query("DELETE FROM {url_alias} WHERE dst = '%s'", "$aprefix$feature_id");
+   path_set_alias("node/$node->nid","$aprefix$feature_id");
+
+   return '';
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_set_vocabulary (){
+
+   //include the file containing the required functions for adding taxonomy vocabs
+   module_load_include('inc', 'taxonomy', 'taxonomy.admin');
+
+   // get the vocabularies so that we make sure we don't recreate
+   // the vocabs that already exist
+   $vocabularies = taxonomy_get_vocabularies();
+   $ft_vid = NULL;
+   $op_vid = NULL;
+   $lb_vid = NULL;
+   $an_vid = NULL;
+
+   // These taxonomic terms are hard coded because we
+   // konw we have these relationships in the chado tables
+   // through foreign key relationships.  The tripal
+   // modules that correspond to these chado "modules" don't
+   // need to be installed for the taxonomy to work.
+   foreach($vocabularies as $vocab){
+      if($vocab->name == 'Feature Type'){
+         $ft_vid = $vocab->vid;
+      }
+      if($vocab->name == 'Organism'){
+         $op_vid = $vocab->vid;
+      }
+      if($vocab->name == 'Library'){
+         $lb_vid = $vocab->vid;
+      }
+      if($vocab->name == 'Analysis'){
+         $an_vid = $vocab->vid;
+      }
+   }
+
+   if(!$ft_vid){
+      $form_state = array();
+      $values = array(
+         'name' => t('Feature Type'),
+         'nodes' => array('chado_feature' => 'chado_feature'),
+         'description' => t('The feature type (or SO cvterm for this feature).'),
+         'help' => t('Select the term that matches the feature '),
+         'tags' => 0,
+         'hierarchy' => 1,
+         'relations' => 1,
+         'multiple' => 0,
+         'required' => 0,
+         'weight' => 1,
+      );
+      drupal_execute('taxonomy_form_vocabulary', $form_state,$values);
+      drupal_execute('taxonomy_form_vocabulary', $form_state);
+   }
+
+   if(!$op_vid){
+      $form_state = array();
+      $values = array(
+         'name' => t('Organism'),
+         'nodes' => array('chado_feature' => 'chado_feature'),
+         'description' => t('The organism to which this feature belongs.'),
+         'help' => t('Select the term that matches the feature '),
+         'tags' => 0,
+         'hierarchy' => 1,
+         'relations' => 1,
+         'multiple' => 0,
+         'required' => 0,
+         'weight' => 2,
+      );
+      drupal_execute('taxonomy_form_vocabulary', $form_state,$values);
+      drupal_execute('taxonomy_form_vocabulary', $form_state);
+   }
+
+   if(!$lb_vid){
+      $form_state = array();
+      $values = array(
+         'name' => t('Library'),
+         'nodes' => array('chado_feature' => 'chado_feature'),
+         'description' => t('Chado features associated with a library are assigned the term associated with the library'),
+         'help' => t('Select the term that matches the feature '),
+         'tags' => 0,
+         'hierarchy' => 1,
+         'relations' => 1,
+         'multiple' => 0,
+         'required' => 0,
+         'weight' => 3,
+      );
+      drupal_execute('taxonomy_form_vocabulary', $form_state, $values);
+      drupal_execute('taxonomy_form_vocabulary', $form_state);
+   }
+
+   if(!$an_vid){
+      $form_state = array();
+      $values = array(
+         'name' => t('Analysis'),
+         'nodes' => array('chado_feature' => 'chado_feature'),
+         'description' => t('Any analysis to which this feature belongs.'),
+         'help' => t('Select the term that matches the feature '),
+         'tags' => 0,
+         'hierarchy' => 1,
+         'relations' => 1,
+         'multiple' => 1,
+         'required' => 0,
+         'weight' => 4,
+      );
+      drupal_execute('taxonomy_form_vocabulary', $form_state,$values);
+      drupal_execute('taxonomy_form_vocabulary', $form_state);
+   }
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_del_vocabulary(){
+   //include the file containing the required functions for adding taxonomy vocabs
+   module_load_include('inc', 'taxonomy', 'taxonomy.admin');
+
+   // get the vocabularies
+   $vocabularies = taxonomy_get_vocabularies();
+
+   // These taxonomic terms are hard coded because we
+   // know we have these relationships in the chado tables
+   // through foreign key relationships.  The tripal
+   // modules that correspond to these chado "modules" don't
+   // need to be installed for the taxonomy to work.
+   foreach($vocabularies as $vocab){
+      if($vocab->name == 'Feature Type'){
+         taxonomy_del_vocabulary($vocab->vid);
+      }
+      if($vocab->name == 'Organism'){
+         taxonomy_del_vocabulary($vocab->vid);
+      }
+      if($vocab->name == 'Library'){
+         taxonomy_del_vocabulary($vocab->vid);
+      }
+      if($vocab->name == 'Analysis'){
+         taxonomy_del_vocabulary($vocab->vid);
+      }
+   }
+
+}
+/************************************************************************
+ *
+ */
+function tripal_features_set_taxonomy($max_sync = 0,$job_id = NULL){
+
+   // make sure our vocabularies are cleaned and reset before proceeding
+   tripal_feature_del_vocabulary();
+   tripal_feature_set_vocabulary();
+
+   // iterate through all drupal feature nodes and set the taxonomy
+   $results = db_query("SELECT * FROM {chado_feature}");
+   $nsql =  "SELECT * FROM {node} ".
+            "WHERE nid = %d";
+   $i = 0;
+
+   // load into ids array
+   $count = 0;
+   $chado_features = array();
+   while($chado_feature = db_fetch_object($results)){
+      $chado_features[$count] = $chado_feature;
+      $count++;
+   }
+
+   // Iterate through features that need to be synced
+   $interval = intval($count * 0.01);
+   foreach($chado_features as $chado_feature){
+
+      // update the job status every 1% features
+      if($job_id and $i % $interval == 0){
+         tripal_job_set_progress($job_id,intval(($i/$count)*100));
+      }
+      $node = db_fetch_object(db_query($nsql,$chado_feature->nid));
+      tripal_feature_set_taxonomy($node,$chado_feature->feature_id);
+
+      $i++;
+   }
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_set_taxonomy ($node,$feature_id){
+
+   // iterate through the taxonomy classes that have been
+   // selected by the admin user and make sure we only set those
+   $tax_classes = variable_get('tax_classes', '');
+   $do_ft = 0;
+   $do_op = 0;
+   $do_lb = 0;
+   $do_an = 0;
+   foreach($tax_classes as $class){
+      if(strcmp($class ,'organism')==0){
+         $do_op = 1;
+      }
+      if(strcmp($class,'feature_type')==0){
+         $do_ft = 1;
+      }
+      if(strcmp($class,'library')==0){
+         $do_lb = 1;
+      }
+      if(strcmp($class,'analysis')==0){
+         $do_an = 1;
+      }
+   }
+
+
+   // get the list of vocabularies and find our two vocabularies of interest
+   $vocabularies = taxonomy_get_vocabularies();
+   $ft_vid = NULL;
+   $op_vid = NULL;
+   $lb_vid = NULL;
+   $an_vid = NULL;
+   foreach($vocabularies as $vocab){
+      if($vocab->name == 'Feature Type'){
+         $ft_vid = $vocab->vid;
+      }
+      if($vocab->name == 'Organism'){
+         $op_vid = $vocab->vid;
+      }
+      if($vocab->name == 'Library'){
+         $lb_vid = $vocab->vid;
+      }
+      if($vocab->name == 'Analysis'){
+         $an_vid = $vocab->vid;
+      }
+   }
+
+   // get the cvterm and the organism for this feature
+   $sql = "SELECT CVT.name AS cvname, O.genus, O.species ".
+          "FROM {CVTerm} CVT ".
+          "  INNER JOIN Feature F on F.type_id = CVT.cvterm_id ".
+          "  INNER JOIN Organism O ON F.organism_id = O.organism_id ".
+          "WHERE F.feature_id = $feature_id";
+   $previous_db = db_set_active('chado');  // use chado database
+   $feature = db_fetch_object(db_query($sql));
+   db_set_active($previous_db);  // now use drupal database
+
+   // Set the feature type for this feature
+   if($do_ft && $ft_vid){
+      $tags["$ft_vid"] = "$feature->cvname";
+   }
+   // Set the organism for this feature type
+   if($do_op && $op_vid){
+      $tags["$op_vid"] = "$feature->genus $feature->species";
+   }
+
+   // get the library that this feature may belong to and add it as taxonomy
+   if($do_lb && $lb_vid){
+      $sql = "SELECT L.name ".
+             "FROM {Library} L ".
+             "  INNER JOIN Library_feature LF ON LF.library_id = L.library_id ".
+             "WHERE LF.feature_id = %d ";
+      $previous_db = db_set_active('chado');  // use chado database
+      $library = db_fetch_object(db_query($sql,$feature_id));
+      db_set_active($previous_db);  // now use drupal database
+      $tags["$lb_vid"] = "$library->name";
+   }
+
+   // get the analysis that this feature may belong to and add it as taxonomy
+   if($do_an && $an_vid){
+      $sql = "SELECT A.name ".
+             "FROM {Analysis} A ".
+             "  INNER JOIN Analysisfeature AF ON AF.analysis_id = A.analysis_id ".
+             "WHERE AF.feature_id = $feature_id ";
+      $results = db_query($sql);
+      $previous_db = db_set_active('chado');  // use chado database
+      $analysis_terms = array();
+      while($analysis=db_fetch_object($results)){
+         // TODO -- how to set more than one taxonmy term ????
+         $analysis_terms[] = "$analysis->name";
+         //           $tags["$an_vid"] = "$analysis->name";
+      }
+      $tags["$an_vid"] = $analysis_terms;
+      db_set_active($previous_db);  // now use drupal database
+   }
+
+   // now add the taxonomy to the node
+   $terms['tags'] = $tags;
+   taxonomy_node_save($node,$terms);
+   //   print "Setting $node->title: " . implode(", ",$tags) . "\n";
+
+}
+/************************************************************************
+ *
+ */
+function tripal_features_reindex ($max_sync,$job_id = NULL){
+   $i = 0;
+
+   // We register a shutdown function to ensure that the nodes
+   // that are indexed will have proper entries in the search_totals
+   // table.  Without these entries, the searching doesn't work
+   // properly. This function may run for quite a while since
+   // it must calculate the sum of the scores of all entries in
+   // the search_index table.  In the case of common words like
+   // 'contig', this will take quite a while
+   register_shutdown_function('search_update_totals');
+
+   // use this SQL statement to get the features that we're going to index. This
+   // SQL statement is derived from the hook_search function in the Drupal API.
+   // Essentially, this is the SQL statement that finds all nodes that need
+   // reindexing, but adjusted to include the chado_feature
+   $sql = "SELECT N.nid, N.title, CF.feature_id ".
+          "FROM {node} N ".
+          "  INNER JOIN chado_feature CF ON CF.nid = N.nid ";
+   $results = db_query($sql);
+
+   // load into ids array
+   $count = 0;
+   $chado_features = array();
+   while($chado_feature = db_fetch_object($results)){
+      $chado_features[$count] = $chado_feature;
+      $count++;
+   }
+
+   // Iterate through features that need to be synced
+   $interval = intval($count * 0.01);
+   foreach($chado_features as $chado_feature){
+
+      // update the job status every 1% features
+      if($job_id and $i % $interval == 0){
+         tripal_job_set_progress($job_id,intval(($i/$count)*100));
+      }
+
+      // sync only the max requested
+      if($max_sync and $i == $max_sync){
+         return '';
+      }
+      tripal_feature_index_feature ($chado_feature->feature_id,$chado_feature->nid);
+      $i++;
+   }
+
+   return '';
+}
+
+/************************************************************************
+ *
+ */
+function tripal_feature_index_feature ($feature_id,$nid){
+   // return if we haven't been provided with a feature_id
+   if(!$feature_id){
+      return 0;
+   }
+
+   // if we only have a feature_id then let's find a corresponding
+   // node.  If we can't find a node then return.
+   if(!$nid){
+      $nsql = "SELECT N.nid,N.title FROM {chado_feature} CF ".
+              "  INNER JOIN {node} N ON N.nid = CF.nid ".
+              "WHERE CF.feature_id = %d";
+      $node = db_fetch_object(db_query($nsql,$feature_id));
+      if(!$node){
+         return 0;
+      }
+      $node = node_load($node->nid);
+   } else {
+      $node = node_load($nid);
+   }
+
+   // node load the noad, the comments and the taxonomy and
+   // index
+   $node->build_mode = NODE_BUILD_SEARCH_INDEX;
+   $node = node_build_content($node, FALSE, FALSE);
+   $node->body = drupal_render($node->content);
+   node_invoke_nodeapi($node, 'view', FALSE, FALSE);
+   $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
+   $node->body .= module_invoke('taxonomy','nodeapi', $node, 'update index');
+   //   print "$node->title: $node->body\n";
+   search_index($node->nid,'node',$node->body);
+   return 1;
+}
+/************************************************************************
+ *
+ */
+function tripal_features_cleanup($dummy = NULL, $job_id = NULL) {
+
+   // build the SQL statments needed to check if nodes point to valid features
+   $dsql = "SELECT * FROM {node} WHERE type = 'chado_feature' order by nid";
+   $nsql = "SELECT * FROM {node} WHERE nid = %d";
+   $csql = "SELECT * FROM {chado_feature} where nid = %d ";
+   $cfsql= "SELECT * FROM {chado_feature}";
+   $tsql = "SELECT * FROM {feature} F ".
+          "  INNER JOIN CVTerm CVT ON F.type_id = CVT.cvterm_id ".
+          "WHERE feature_id = %d  AND (";
+   $supported_ftypes = split("[ \n]",variable_get('chado_feature_types','EST contig'));
+   foreach($supported_ftypes as $ftype){
+      $tsql .= " CVT.name = '$ftype' OR ";
+   }
+   $tsql .= " 0=1) ";  // add a 0=1 just as a filler so we don't have to remove a trailing 'OR'
+
+   // load into nodes array
+   $results = db_query($dsql);
+   $count = 0;
+   $nodes = array();
+   while($node = db_fetch_object($results)){
+      $nodes[$count] = $node;
+      $count++;
+   }
+
+   // load the chado_features into an array
+   $results = db_query($cfsql);
+   $cnodes = array();
+   while($node = db_fetch_object($results)){
+      $cnodes[$count] = $node;
+      $count++;
+   }
+   $interval = intval($count * 0.01);
+
+   // iterate through all of the chado_feature nodes and delete those  that aren't valid
+   foreach($nodes as $nid){
+
+      // update the job status every 1% features
+      if($job_id and $i % $interval == 0){
+         tripal_job_set_progress($job_id,intval(($i/$count)*100));
+      }
+
+      // first check to see if the node has a corresponding entry
+      // in the chado_feature table. If not then delete the node.
+      $feature = db_fetch_object(db_query($csql,$nid->nid));
+      if(!$feature){
+         node_delete($nid->nid);
+         $message = "Missing in chado_feature table.... DELETING: $nid->nid\n";
+         watchdog('tripal_feature',$message,array(),WATCHDOG_WARNING);
+         continue;
+      }
+
+      // second check to see if the node is for a feature of an allowed type.
+      // if not, then delete the node.  This check will also take care of the
+      // case when a node exists and an entry in the chado_feature table exists
+      // but no feature with a matching feature_id exists
+      $previous_db = db_set_active('chado');  // use chado database
+      $ftype = db_fetch_object(db_query($tsql,$feature->feature_id));
+      db_set_active($previous_db);  // now use drupal database
+
+      if(!$ftype){
+         node_delete($nid->nid);
+         db_query("DELETE FROM {chado_feature} WHERE feature_id = $feature->feature_id");
+         $message = "Node of the wrong feature type.... DELETING: $nid->nid\n";
+         watchdog('tripal_feature',$message,array(),WATCHDOG_WARNING);
+      }
+      $i++;
+   }
+
+   // iterate through all of the chado_feature nodes and delete those  that aren't valid
+   foreach($cnodes as $nid){
+      // update the job status every 1% features
+      if($job_id and $i % $interval == 0){
+         tripal_job_set_progress($job_id,intval(($i/$count)*100));
+      }
+      $node = db_fetch_object(db_query($nsql,$nid->nid));
+      if(!$node){
+         db_query("DELETE FROM {chado_feature} WHERE nid = $nid->nid");
+         $message = "chado_feature missing node.... DELETING: $nid->nid\n";
+         watchdog('tripal_feature',$message,array(),WATCHDOG_WARNING);
+      }
+
+      $i++;
+   }
+   return '';
+}
+
+/************************************************************************
+ *
+ */
+function tripal_feature_bulkload(){
+   return drupal_get_form('tripal_feature_load_fasta_form');
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_load_fasta_form (&$form_state = NULL){
+
+   // get the list of organisms
+   $sql = "SELECT * FROM {Organism} ORDER BY genus,species";
+   $previous_db = db_set_active('chado');  // use chado database
+   $org_rset = db_query($sql);
+   db_set_active($previous_db);  // now use drupal database
+   $organisms = array();
+   $organisms[''] = '';
+   while($organism = db_fetch_object($org_rset)){
+      $organisms[$organism->organism_id] = "$organism->genus $organism->species ($organism->common_name)";
+   }
+
+   // get the list of supported feature types
+   $ftypes = array();
+   $ftypes[''] = '';
+   $supported_ftypes = split("[ \n]",variable_get('chado_feature_feature_types','EST contig'));
+   foreach($supported_ftypes as $ftype){
+      $ftypes["$ftype"] = $ftype;
+   }
+
+   // get the list of libraries
+   // TODO !!!! Use Ajax to filter this automatically based on the organism
+   // selected by the user. This will prevent mistakes from user input.
+   $sql = "SELECT * FROM {Library} L ".
+          "  INNER JOIN Organism O ON L.organism_id = O.organism_id ".
+          "ORDER BY L.name";
+   $previous_db = db_set_active('chado');  // use chado database
+   $lib_rset = db_query($sql);
+   db_set_active($previous_db);  // now use drupal database
+   $libraries = array();
+   $libraries[''] = '';
+   while($library = db_fetch_object($lib_rset)){
+      $libraries[$library->library_id] = "$library->name ($library->genus $library->species)";
+   }
+
+   $form['#attributes']['enctype'] = 'multipart/form-data';
+
+   $form['organism'] = array (
+      '#title'       => t('Organism'),
+      '#type'        => t('select'),
+      '#description' => t("Choose the organism with which these sequences are associated "),
+      '#required'    => TRUE,
+      '#default_vaule' => '',
+      '#options'     => $organisms,
+      '#weight'      => 1,
+   );
+   $form['library'] = array (
+      '#title'       => t('Library'),
+      '#type'        => t('select'),
+      '#description' => t("Choose the library with from which these sequences are derived. Leave blank if not applicable."),
+      '#required'    => FALSE,
+      '#default_vaule' => '',
+      '#options'     => $libraries,
+      '#weight'      => 2,
+   );
+   $form['ftype'] = array (
+      '#title'       => t('Feature Type'),
+      '#type'        => t('select'),
+      '#description' => t("Choose the category of sequences you are uploading.  All sequences in the FASTA file will be imported as this type"),
+      '#required'    => TRUE,
+      '#default_vaule' => '',
+      '#options'     => $ftypes,
+      '#weight'      => 3,
+   );
+   $form['fasta_file'] = array(
+      '#type'       => t('file'),
+      '#title'      => t('Fasta File'),
+      '#description'   => t('Upload a FASTA file of sequences. The definition line should contain only the feature name. All other annotations should be removed. The file must not be larger than ' . file_upload_max_size() . ' bytes'),
+      '#weight'        => 4,
+   );
+   $form['upload'] = array(
+      '#type' => 'submit',
+      '#value' => t('Upload File'),
+      '#weight' => 2,
+      '#executes_submit_callback' => TRUE,
+      '#weight'      => 5,
+   );
+
+   return $form;
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_load_fasta_form_validate($form, &$form_state){
+
+   // TODO !!! check that the fasta file is valid
+
+   global $user;
+
+   // we need a path within the drupal installation to temporarily use as the destination
+   // after we upload, we'll move the file to analysis directory
+   $upload_url = file_directory_path() . "/chado_feature_bulk_upload/$user->uid";
+
+   // create the download directory. We do it this way rather than the
+   // file_check_directory because we don't want a drupal message presented
+   // the user when the directory is created.
+   if (!is_dir($upload_url)) {
+      mkdir($upload_url,0775,TRUE);
+   }
+
+   // upload the file and copy it to the proper location
+   $validators = array();  // we don't have any validators
+   if($file = file_save_upload('fasta_file',$validators,$upload_url)){
+      drupal_set_message("File $file->name uploaded succesfully");
+   } else {
+      form_set_error('fasta_file',t('Upload Failed'));
+   }
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_load_fasta_form_submit($form, &$form_state){
+   global $user;
+
+   // add a job to be executed
+   tripal_add_job ($job_name,$type,$callback,$uid);
+}
+/************************************************************************
+ *
+ */
+function tripal_feature_return_fasta($feature,$desc){   
+   $fasta  = ">" . variable_get('chado_feature_accession_prefix','ID') . "$feature->feature_id|$feature->name";
+   $fasta .= " $desc\n";
+   $fasta .= wordwrap($feature->residues, 50, "\n", true);
+   $fasta .= "\n\n";
+   return $fasta;
+}
+