Browse Source

Merge branch '7.x-3.x' into 7.x-3.x-remotedata

Stephen Ficklin 7 years ago
parent
commit
f9f66211d7
32 changed files with 1886 additions and 175 deletions
  1. 169 0
      tripal/api/tripal.collections.api.inc
  2. 1 1
      tripal/api/tripal.entities.api.inc
  3. 39 0
      tripal/api/tripal.fields.api.inc
  4. 472 0
      tripal/includes/TripalEntityCollection.inc
  5. 0 5
      tripal/includes/TripalFieldDownloader/TripalFASTADownloader.inc
  6. 0 114
      tripal/includes/TripalFieldDownloader/TripalFieldDownloader.inc
  7. 0 5
      tripal/includes/TripalFieldDownloader/TripalGFFDownloader.inc
  8. 0 5
      tripal/includes/TripalFieldDownloader/TripalTabCSVDownloader.inc
  9. 0 5
      tripal/includes/TripalFieldDownloader/TripalTabDownloader.inc
  10. 65 0
      tripal/includes/TripalFieldDownloaders/TripalCSVDownloader.inc
  11. 178 0
      tripal/includes/TripalFieldDownloaders/TripalFieldDownloader.inc
  12. 27 0
      tripal/includes/TripalFieldDownloaders/TripalGFF3Downloader.inc
  13. 115 0
      tripal/includes/TripalFieldDownloaders/TripalNucFASTADownloader.inc
  14. 114 0
      tripal/includes/TripalFieldDownloaders/TripalProteinFASTADownloader.inc
  15. 65 0
      tripal/includes/TripalFieldDownloaders/TripalTabDownloader.inc
  16. 7 5
      tripal/includes/TripalFields/TripalField.inc
  17. 0 5
      tripal/includes/TripalFields/TripalFieldFormatter.inc
  18. 65 0
      tripal/includes/tripal.admin.inc
  19. 114 0
      tripal/includes/tripal.collections.inc
  20. 12 3
      tripal/includes/tripal.fields.inc
  21. 3 2
      tripal/theme/js/TripalUploader.js
  22. 1 0
      tripal/tripal.info
  23. 96 20
      tripal/tripal.install
  24. 87 3
      tripal/tripal.module
  25. 7 0
      tripal/tripal.views.inc
  26. 6 1
      tripal/tripal.views_default.inc
  27. 221 0
      tripal/views_handlers/tripal_views_handler_area_collections.inc
  28. 1 1
      tripal_bulk_loader/tripal_bulk_loader.module
  29. 1 0
      tripal_chado/api/tripal_chado.semweb.api.inc
  30. 6 0
      tripal_chado/includes/TripalFields/ChadoField.inc
  31. 7 0
      tripal_chado/includes/TripalFields/data__protein_sequence/data__protein_sequence.inc
  32. 7 0
      tripal_chado/includes/TripalFields/data__sequence/data__sequence.inc

+ 169 - 0
tripal/api/tripal.collections.api.inc

@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * Creates a collection of entities for a given user.
+ *
+ * @param  $details
+ *   An association array containing the details for a collection. The
+ *   details must include the following key/value pairs:
+ *   - uid:  The ID of the user that owns the collection
+ *   - collection_name:  The name of the collection
+ *   - bundle_name:  The name of the TripalEntity content type.
+ *   - ids:  An array of the entity IDs that form the collection.
+ *   - fields: An array of the field IDs that the collection is limited to.
+ *   - description:  A user supplied description for the collection.
+ * @return TripalEntityCollection
+ *   An instance of a TripalEntityCollection class or FALSE on failure.
+ */
+function tripal_create_collection($details) {
+  global $user;
+
+  try {
+    $collection = new TripalEntityCollection();
+    $collection->create($details);
+    $collection_id = $collection->getCollectionID();
+
+    // Add the job to write the collection download files.
+    $args = array($collection_id);
+    tripal_add_job('Create data collection files for ' . $user->name, 'tripal',
+        'tripal_create_collection_files', $args, $user->uid, 10, array());
+
+
+    drupal_set_message(t("Collection '%name' created with %num_recs record(s).  Downloadble files will be available shortly.  Check the !view for status and download links.",
+      array(
+        '%name' => $details['collection_name'],
+        '%num_recs' => count($details['ids']),
+        '!view' => l('data collections page', 'user/' . $user->uid . '/data-collections'),
+      ))
+    );
+
+  }
+  catch (Exception $e) {
+    drupal_set_message(t("Failed to create the collection '%name': " . $e->getMessage(), array('%name' =>  $details['collection_name'])), 'error');
+    return FALSE;
+  }
+  return $collection;
+}
+
+/**
+ * Retrieves an array of collections for a user.
+ *
+ * @param $uid
+ *   The User ID
+ * @return
+ *   An array of TripalEntityCollection objects.
+ */
+function tripal_get_user_collections($uid) {
+  if (!$uid) {
+    throw new Exception('tripal_get_user_collections: Missing the $uid argument.');
+  }
+  $user = user_load($uid);
+  if (!$user) {
+    throw new Exception('tripal_get_user_collections: Invalid $uid provided.');
+  }
+
+  $collections = array();
+  $results = db_select('tripal_collection', 'tc')
+    ->fields('tc', array('collection_id'))
+    ->condition('uid', $uid)
+    ->execute();
+  while ($collection_id = $results->fetchField()) {
+    $collection = new TripalEntityCollection();
+    $collection->load($collection_id);
+    $collections[] = $collection;
+  }
+  return $collections;
+}
+
+/**
+ * Deletes all collections that have surpassed their lifespan
+ */
+function tripal_expire_collections() {
+  $max_hours = variable_get('tripal_data_collections_lifespan', 7);
+  $ctime = time();
+
+  $query = db_select('tripal_collection', 'tc');
+  $query->fields('tc', array('collection_id'));
+  $query->where("(($ctime - create_date) / 60) / 60 >= $max_hours");
+  $results = $query->execute();
+  while ($collection_id = $results->fetchField()) {
+    $collection = new TripalEntityCollection();
+    $collection->load($collection_id);
+    $collections[] = $collection;
+    $collection->delete();
+  }
+  return $collections;
+}
+
+/**
+ * Retrieve a collection using the collection ID
+ *
+ * @param $values
+ *   An array of key/value pairs to uniquely identify a collection.  The
+ *   following keys can be used:
+ *   - collection_id:  The numeric value for the collection.
+ *   - uid: The ID of the user that owns the collection. This key must
+ *     always be used with the 'name' key.
+ *   - name:  The name of the collection.  This key must always be used
+ *     with the 'uid' key.
+ *
+ * @return
+ *  An instance of a TripalEntityCollection class or FALSE on failure.
+ *
+ */
+function tripal_get_collection($values) {
+  $collection_id = array_key_exists('collection_id', $values) ? $values['collection_id'] : NULL;
+  $uid = array_key_exists('uid', $values) ? $values['uid'] : NULL;
+  $name = array_key_exists('name', $values) ? $values['name'] : NULL;
+
+  if ($uid and !$name) {
+    throw new Exception('tripal_get_collection: Missing the collection name when specifying the User ID.');
+  }
+  if (!$uid and $name) {
+    throw new Exception('tripal_get_collection: Missing the User ID when specifying the collection name.');
+  }
+  if (!$collection_id and !$uid and !$name) {
+    throw new Exception('tripal_get_collection: Missing a valid key.');
+  }
+
+  if ($name and $uid) {
+    $collection_id = db_select('tripal_collection', 'tc')
+      ->fields('tc', array('collection_id'))
+      ->condition('uid', $uid)
+      ->condition('collection_name', $name)
+      ->execute()
+      ->fetchField();
+    if (!$collection_id) {
+      throw new Exception('tripal_get_collection: The collection could not be found with the given uid and name.');
+    }
+  }
+
+  try {
+    $collection = new TripalEntityCollection();
+    $collection->load($collection_id);
+    return $collection;
+  }
+  catch (Exception $e) {
+    drupal_set_message(t("Failed to load the collection with id '%id': " . $e->getMessage(), array('%id' =>  $collection_id)), 'error');
+    return FALSE;
+  }
+}
+
+/**
+ * Generates the downloadable files for a Collection
+ *
+ * @param TripalEntityCollection $collection
+ */
+function tripal_create_collection_files($collection_id, TripalJob $job = NULL) {
+   if($job) {
+     $job->setProgress(0);
+   }
+
+   $collection = new TripalEntityCollection();
+   $collection->load($collection_id);
+   $collection->writeAll();
+
+   if ($job) {
+     $job->setProgress(100);
+   }
+}

+ 1 - 1
tripal/api/tripal.entities.api.inc

@@ -18,7 +18,7 @@
  * @param $reset: Whether to reset the internal cache for the requested entity
  *   type. Defaults to FALSE.
  * @param $field_ids
- *   A list of numeric feild IDs that should be loaded.  The
+ *   A list of numeric field IDs that should be loaded.  The
  *   TripalField named 'content_type' is always automatically added.
  *
  * @return

+ 39 - 0
tripal/api/tripal.fields.api.inc

@@ -162,6 +162,7 @@ function tripal_get_field_widgets() {
   }
   return $widgets;
 }
+
 /**
  * Retrieves a list of TripalFieldFormatters.
  *
@@ -259,6 +260,44 @@ function tripal_load_include_field_class($class) {
   return FALSE;
 }
 
+/**
+ * Loads the TripalEntityDownloader file into scope.
+ *
+ * @param $class
+ *   The name of the class to include.
+ *
+ * @return
+ *   TRUE if the downloader class file was found, FALSE otherwise.
+ */
+function tripal_load_include_downloader_class($class) {
+
+  $modules = module_list(TRUE);
+  foreach ($modules as $module) {
+    $file_path = realpath(".") . '/' . drupal_get_path('module', $module) . '/includes/TripalFieldDownloaders/' . $class . '.inc';
+    if (file_exists($file_path)) {
+      module_load_include('inc', $module, 'includes/TripalFieldDownloaders/' . $class);
+      if (class_exists($class)) {
+        return TRUE;
+      }
+    }
+  }
+
+  // If the libraries module is enabled then we want to look for a
+  // TripalFieldDownloader library folder and see if our field exist there.
+  if (module_exists('libraries')) {
+    $library_path = libraries_get_path('TripalFieldDownloaders');
+    $file_path = realpath(".") . '/' . $library_path .'/' . $class . '.inc';
+    if (file_exists($file_path)) {
+      require_once($file_path);
+      if (class_exists($class)) {
+        return TRUE;
+      }
+    }
+  }
+
+  return FALSE;
+}
+
 /**
  * More easily get the value of a single item from a field's items array.
  *

+ 472 - 0
tripal/includes/TripalEntityCollection.inc

@@ -0,0 +1,472 @@
+<?php
+
+class TripalEntityCollection {
+
+  /**
+   * The name of the bundle (i.e. content type) to which the entities belong.
+   */
+  protected $bundle_name = '';
+
+  /**
+   * The collection ID
+   */
+  protected $collection_id = NULL;
+
+  /**
+   * The name of this collection.
+   */
+  protected $collection_name = '';
+  /**
+   * An array of numeric entities IDs.
+   */
+  protected $ids = array();
+  /**
+   * An array of field IDs.
+   */
+  protected $fields = array();
+  /**
+   * The user object of the user that owns the collection.
+   */
+  protected $user = array();
+  /**
+   * The date that the collection was created.
+   */
+  protected $create_date = '';
+
+  /**
+   * The list of downloaders available for this bundle.
+   */
+  protected $downloaders = array();
+
+  /**
+   * The description for this collection.
+   */
+  protected $description = '';
+
+  /**
+   * Constructs a new instance of the TripalEntityCollection class.
+   */
+  public function __construct() {
+
+  }
+
+  /**
+   * Deletes the current collection
+   */
+  public function delete() {
+
+    if (!$this->collection_id) {
+      throw new Exception('This data collection object has not yet been loaded. Cannot delete.');
+    }
+
+    try {
+      db_delete('tripal_collection')
+        ->condition('collection_id', $this->collection_id)
+        ->execute();
+
+      // Remove any files that may have been created
+      foreach ($this->downloaders as $class_name => $label) {
+        tripal_load_include_downloader_class($class_name);
+        $outfile = $this->getOutfile($class_name);
+        $downloader = new $class_name($this->bundle_name, $this->ids, $this->fields,
+            $outfile, $this->getUserID());
+        $downloader->delete();
+      }
+
+      // Reset the class to defaults.
+      $this->collection_id = NULL;
+      $this->bundle_name = '';
+      $this->collection_name = '';
+      $this->create_date = '';
+      $this->description = '';
+      $this->fields = array();
+      $this->ids = array();
+
+    }
+    catch (Exception $e) {
+      throw new Exception('Cannot delete collection: ' .  $e->getMessage());
+    }
+  }
+
+  /**
+   * Loads an existing collection using a collection ID.
+   *
+   * @param $collection_id
+   *   The ID of the collection to load.
+   *
+   * @throws Exception
+   */
+  public function load($collection_id) {
+
+    // Make sure we have a numeric job_id.
+    if (!$collection_id or !is_numeric($collection_id)) {
+      throw new Exception("You must provide the collection_id to load the collection.");
+    }
+
+    $collection = db_select('tripal_collection', 'tc')
+      ->fields('tc')
+      ->condition('collection_id', $collection_id)
+      ->execute()
+      ->fetchObject();
+
+    if (!$collection) {
+      throw new Exception("Cannot find a collection with the ID provided.");
+    }
+
+    // Fix the date/time fields.
+    $this->bundle_name = $collection->bundle_name;
+    $this->collection_name = $collection->collection_name;
+    $this->create_date = $collection->create_date;
+    $this->user = user_load($collection->uid);
+    $this->ids = unserialize($collection->ids);
+    $this->fields = unserialize($collection->fields);
+    $this->description = $collection->description;
+    $this->collection_id = $collection->collection_id;
+
+    // Iterate through the fields and find out what download formats are
+    // supported for this basket.
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      if (!$field) {
+        continue;
+      }
+      $field_name = $field['field_name'];
+      $field_type = $field['type'];
+      $field_module = $field['module'];
+      $instance = field_info_instance('TripalEntity', $field_name, $this->bundle_name);
+      $downloaders = array();
+
+      // All fields should support the Tab and CSV downloaders.
+      tripal_load_include_downloader_class('TripalTabDownloader');
+      $this->downloaders['TripalTabDownloader'] = TripalTabDownloader::$label;
+      tripal_load_include_downloader_class('TripalCSVDownloader');
+      $this->downloaders['TripalCSVDownloader'] = TripalCSVDownloader::$label;
+
+      if (tripal_load_include_field_class($field_type)) {
+        $settings = $field_type::$default_instance_settings;
+        if (array_key_exists('download_formatters', $settings)) {
+          foreach ($settings['download_formatters'] as $class_name) {
+            if (!array_key_exists($class_name, $this->downloaders)) {
+              tripal_load_include_downloader_class($class_name);
+              $this->downloaders[$class_name] = $class_name::$label;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Creates a new collection.
+   *
+   * @param  $details
+   *   An association array containing the details for a collection. The
+   *   details must include the following key/value pairs:
+   *   - uid:  The ID of the user that owns the collection
+   *   - collection_name:  The name of the collection
+   *   - bundle_name:  The name of the TripalEntity content type.
+   *   - ids:  An array of the entity IDs that form the collection.
+   *   - fields: An array of the field IDs that the collection is limited to.
+   *   - description:  A user supplied description for the collection.
+   *
+   * @throws Exception
+   */
+  public function create($details) {
+    if (!$details['uid']) {
+      throw new Exception("Must provide a 'uid' key to TripalEntityCollection::create().");
+    }
+    if (!$details['collection_name']) {
+      throw new Exception("Must provide a 'collection_name' key to TripalEntityCollection::create().");
+    }
+    if (!$details['bundle_name']) {
+      throw new Exception("Must provide a 'bundle_name' key to TripalEntityCollection::create().");
+    }
+    if (!$details['ids']) {
+      throw new Exception("Must provide a 'ids' key to TripalEntityCollection::create().");
+    }
+    if (!$details['fields']) {
+      throw new Exception("Must provide a 'fields' key to TripalEntityCollection::create().");
+    }
+
+    if (!is_array($details['ids'])) {
+      throw new Exception("The 'ids' key must be an array key to TripalEntityCollection::create().");
+    }
+    if (!is_array($details['fields'])) {
+      throw new Exception("The 'ids' key must be an array key to TripalEntityCollection::create().");
+    }
+
+    // Before inserting the new collection make sure we don't violote the unique
+    // constraint that a user can only have one collection of the give name.
+    $has_match = db_select('tripal_collection', 'tc')
+      ->fields('tc', array('collection_id'))
+      ->condition('uid', $details['uid'])
+      ->condition('collection_name', $details['collection_name'])
+      ->execute()
+      ->fetchField();
+    if ($has_match) {
+      throw new Exception('Cannot create the collection. One with this name already exists');
+    }
+
+    try {
+      $collection_id = db_insert('tripal_collection')
+        ->fields(array(
+          'collection_name' => $details['collection_name'],
+          'bundle_name' => $details['bundle_name'],
+          'ids' => serialize($details['ids']),
+          'fields' => serialize($details['fields']),
+          'create_date' => time(),
+          'uid' => $details['uid'],
+          'description' => array_key_exists('description', $details) ? $details['description'] : '',
+        ))
+        ->execute();
+      // Now load the job into this object.
+      $this->load($collection_id);
+    }
+    catch (Exception $e) {
+      throw new Exception('Cannot create collection: ' .  $e->getMessage());
+    }
+  }
+
+  /**
+   * Retrieves the list of appropriate download formatters for the basket.
+   *
+   * @return
+   *   An associative array where the key is the TripalFieldDownloader class
+   *   name and the value is the human-readable lable for the formatter.
+   */
+  public function getDownloadFormatters() {
+     return $this->downloaders;
+  }
+
+  /**
+   * Retrieves the list of entity IDs.
+   *
+   * @return
+   *   An array of numeric enity IDs.
+   */
+  public function getEntityIDs(){
+    return $this->ids;
+  }
+
+  /**
+   * Retrieves the list of fields in the basket.
+   *
+   * @return
+   *   An array of numeric field IDs.
+   */
+  public function getFields() {
+    return $this->fields;
+  }
+
+  /**
+   * Retrieves the date that the basket was created.
+   *
+   * @param $formatted
+   *   If TRUE then the date time will be formatted for human readability.
+   * @return
+   *   A UNIX time stamp string containing the date or a human-readable
+   *   string if $formatted = TRUE.
+   */
+  public function getCreateDate($formatted = TRUE) {
+    if ($formatted) {
+      return format_date($this->create_date);
+    }
+    return $this->create_date;
+  }
+
+  /**
+   * Retreives the name of the collection.
+   *
+   * @return
+   *   A string containing the name of the collection.
+   */
+  public function getName() {
+    return $this->collection_name;
+  }
+
+  /**
+   * Retrieves the collection ID.
+   *
+   * @return
+   *   A numeric ID for this collection.
+   */
+  public function getCollectionID(){
+    return $this->collection_id;
+  }
+
+  /**
+   * Retreives the collection description
+   *
+   * @return
+   *   A string containing the description of the collection.
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * Retrieves the user object of the user that owns the collection
+   *
+   * @return
+   *   A Drupal user object.
+   */
+  public function getUser() {
+    return $this->user;
+  }
+
+  /**
+   * Retrieves the ID of the user that owns the collection
+   *
+   * @return
+   *   The numeric User ID.
+   */
+  public function getUserID() {
+    if ($this->user) {
+      return $this->user->uid;
+    }
+    return NULL;
+  }
+
+  /**
+   * Retrieves the output filename for the desired formatter.
+   *
+   * @param $formatter
+   *   The class name of the formatter to use.  The formatter must
+   *   be compatible with the data collection.
+   *
+   * @throws Exception
+   */
+  public function getOutfile($formatter) {
+    if(!$this->isFormatterCompatible($formatter)) {
+      throw new Exception(t('The formatter, "%formatter", is not compatible with this data collection.', array('%formatter' => $formatter)));
+    }
+
+    if (!tripal_load_include_downloader_class($formatter)) {
+      throw new Exception(t('Cannot find the formatter named "@formatter".', array('@formatter', $formatter)));
+    }
+
+    $extension = $formatter::$default_extension;
+    $create_date = $this->getCreateDate(FALSE);
+    $outfile = preg_replace('/[^\w]/', '_', ucwords($this->collection_name)) . '_collection' . '_' . $create_date . '.' . $extension;
+
+    return $outfile;
+  }
+
+  /**
+   * Indicates if the given formatter is compatible with the data collection.
+   *
+   * @param $formatter
+   *   The class name of the formatter to check.
+   * @return boolean
+   *   TRUE if the formatter is compatible, FALSE otherwise.
+   */
+  public function isFormatterCompatible($formatter) {
+    foreach ($this->downloaders as $class_name => $label) {
+      if ($class_name == $formatter) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Writes the collection to all file downloadable formats.
+   *
+   * @throws Exception
+   */
+  public function writeAll() {
+    foreach ($this->downloaders as $class_name => $lable) {
+      $this->write($class_name);
+    }
+  }
+
+  /**
+   * Retrieves the URL for the downloadable file.
+   *
+   * @param $formatter
+   *   The name of the class
+   */
+  public function getOutfileURL($formatter) {
+    $outfile = $this->getOutfilePath($formatter);
+    return file_create_url($outfile);
+  }
+
+  /**
+   * Retrieves the path for the downloadable file.
+   *
+   * The path is in the Drupal URI format.
+   *
+   * @param $formatter
+   *   The name of the class
+   */
+  public function getOutfilePath($formatter) {
+    if(!$this->isFormatterCompatible($formatter)) {
+      throw new Exception(t('The formatter, "@formatter", is not compatible with this data collection.', array('@formatter' => $formatter)));
+
+    }
+
+    if (!tripal_load_include_downloader_class($formatter)) {
+      throw new Exception(t('Cannot find the formatter named "@formatter".', array('@formatter', $formatter)));
+    }
+
+    $outfile = $this->getOutfile($formatter);
+
+    $downloader = new $formatter($this->bundle_name, $this->ids, $this->fields, $outfile, $this->user->uid);
+
+    return $downloader->getURL();
+  }
+
+  /**
+   * Writes the collection to a file.
+   *
+   * @param formatter
+   *   The name of the formatter class to use (e.g. TripalTabDownloader). The
+   *   formatter must be compatible with the data collection.
+   *
+   * @throws Exception
+   */
+  public function write($formatter) {
+
+    if(!$this->isFormatterCompatible($formatter)) {
+      throw new Exception(t('The formatter, "@formatter", is not compatible with this data collection.', array('@formatter' => $formatter)));
+
+    }
+
+    if (!tripal_load_include_downloader_class($formatter)) {
+      throw new Exception(t('Cannot find the formatter named "@formatter".', array('@formatter', $formatter)));
+    }
+
+    $outfile = $this->getOutfile($formatter);
+
+    // Filter out fields that aren't supported by the formatter.
+    $supported_fields = array();
+    foreach ($this->fields as $field_id) {
+      // If the formatter is TripalTabDownloader or TripalCSVDownloader then
+      // we always want to support the field.
+      if ($formatter == 'TripalTabDownloader' or $formatter == 'TripalCSVDownloader') {
+        if (!in_array($field_id, $supported_fields)) {
+          $supported_fields[] = $field_id;
+        }
+        continue;
+      }
+
+      // Otherwise, find out if the formatter specified is supporte by the
+      // field and if so then add it to our list of supported fields.
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+      $field_type = $field['type'];
+      if (tripal_load_include_field_class($field_type)) {
+        $settings = $field_type::$default_instance_settings;
+        if (array_key_exists('download_formatters', $settings)) {
+          if (in_array($formatter, $settings['download_formatters'])) {
+            $supported_fields[] = $field_id;
+          }
+        }
+      }
+    }
+
+    $downloader = new $formatter($this->bundle_name, $this->ids, $supported_fields, $outfile, $this->user->uid);
+    $downloader->write();
+
+  }
+}

+ 0 - 5
tripal/includes/TripalFieldDownloader/TripalFASTADownloader.inc

@@ -1,5 +0,0 @@
-<?php
-
-class TripalFASTADownloader extends TripalFieldDownloader {
-
-}

+ 0 - 114
tripal/includes/TripalFieldDownloader/TripalFieldDownloader.inc

@@ -1,114 +0,0 @@
-<?php
-
-
-class TripalFieldDownloader {
-
-  /**
-   * The bundle name.
-   * @var string
-   */
-  protected $bundle_name = '';
-
-  /**
-   * A set of entity IDs. The entities must all be of the same bundle type.
-   */
-  protected $entity_ids = array();
-
-  /**
-   * The set of fields
-   */
-  protected $fields = array();
-
-  /**
-   * The output file URI.
-   */
-  protected $outfile = '';
-
-  /**
-   * Constructs a new instance of the TripalFieldDownloader class.
-   * @param $bundle_name
-   *   The name of the bundle to which the IDs in the $id argument belong.
-   * @param $ids
-   *   An array of entity IDs
-   * @param $fields
-   *   An array of numeric field IDs to use when constructing the download. If
-   *   no fields are provided then all fields that are appropriate for the
-   *   given type will be used.
-   * @param $outfile_name
-   *   The name of the output file to create (minus any extension).
-   * @param $extension
-   *   The extension to add to the end of the output file.
-   */
-  public function __construct($bundle_name, $ids, $fields = array(),
-      $outfile_name = '', $extension = 'txt') {
-    global $user;
-
-    $this->entity_ids = $ids;
-    $this->fields = $fields;
-
-    // Make sure the user directory exists
-    $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;
-    }
-
-    if (!$outfile_name) {
-      $outfile_name = unqiueid();
-    }
-
-    $this->outfile = $user_dir. '/' . $outfile_name . '.' . $outfile_ext;
-  }
-
-  /**
-   * Retrieves the URL for the downloadable file.
-   */
-  public function getURL() {
-     return $this0>outfile;
-  }
-
-  /**
-   * Creates the download able file.
-   */
-  public function create() {
-    $fh = fopen($this->outfile, "w");
-    foreach ($this->entity_ids as $entity_id) {
-      $entity = tripal_load_entity('TripalEntity', array($entity_id), FALSE, $this->fields);
-      $content = $this->format($entity, $this->fields);
-      fwrite($fh, $content);
-    }
-    fclose($fh);
-  }
-
-  /**
-   * Setups a download stream for the file.
-   */
-  public function download() {
-
-  }
-
-  /**
-   * Formats the output for a given entity.
-   *
-   * This function should be implemented by a child class. It should iterate
-   * over the fields for the entity and return the appropriate format.
-   *
-   * @param $entity
-   *   The entity object.  The fields that should be formatted are already
-   *   loaded.
-   * @param $fields
-   *   A list of field names that should be formatted.
-   *
-   * @return
-   *   A string containing the formatted output.
-   */
-  protected function format($entity, $fields) {
-
-  }
-}

+ 0 - 5
tripal/includes/TripalFieldDownloader/TripalGFFDownloader.inc

@@ -1,5 +0,0 @@
-<?php
-
-class TripalGFFDownloader extends TripalFieldDownloader {
-
-}

+ 0 - 5
tripal/includes/TripalFieldDownloader/TripalTabCSVDownloader.inc

@@ -1,5 +0,0 @@
-<?php
-
-class TripalCSVDownloader extends TripalFieldDownloader {
-
-}

+ 0 - 5
tripal/includes/TripalFieldDownloader/TripalTabDownloader.inc

@@ -1,5 +0,0 @@
-<?php
-
-class TripalTabDownloader extends TripalFieldDownloader {
-
-}

+ 65 - 0
tripal/includes/TripalFieldDownloaders/TripalCSVDownloader.inc

@@ -0,0 +1,65 @@
+<?php
+
+class TripalCSVDownloader extends TripalFieldDownloader {
+  /**
+   * Sets the label shown to the user describing this formatter.
+   */
+  static public $label = 'CSV (comma separated)';
+
+  /**
+   * Indicates the default extension for the outputfile.
+   */
+  static public $default_extension = 'csv';
+
+  /**
+   * @see TripalFieldDownloader::format()
+   */
+  protected function formatEntity($entity) {
+    $row = array();
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+
+      if (!property_exists($entity, $field_name)) {
+        continue;
+      }
+
+      // If we only have one element then this is good.
+      if (count($entity->{$field_name}['und']) == 1) {
+        $value = $entity->{$field_name}['und'][0]['value'];
+        // If the single element is not an array then this is good.
+        if (!is_array($value)) {
+          $row[] = '"' . $value . '"';
+        }
+        else {
+          if (array_key_exists('rdfs:label', $entity->{$field_name}['und'][0]['value'])) {
+            $row[] = strip_tags($entity->{$field_name}['und'][0]['value']['rdfs:label']);
+          }
+          else {
+            $row[] = '';
+          }
+          // TODO: What to do with fields that are arrays?
+        }
+      }
+      else {
+        $row[] = '';
+        // TODO: What to do with fields that have multiple values?
+      }
+    }
+    return array(implode(',', $row));
+  }
+
+  /**
+   * @see TripalFieldDownloader::getHeader()
+   */
+  protected function getHeader() {
+    $row = array();
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+      $instance = field_info_instance('TripalEntity', $field_name, $this->bundle_name);
+      $row[] = '"' . $instance['label'] . '"';
+    }
+    return array(implode(',', $row));
+  }
+}

+ 178 - 0
tripal/includes/TripalFieldDownloaders/TripalFieldDownloader.inc

@@ -0,0 +1,178 @@
+<?php
+
+
+abstract class TripalFieldDownloader {
+
+  /**
+   * Sets the label shown to the user describing this formatter.
+   */
+  static public $label = 'Generic';
+
+  /**
+   * Indicates the default extension for the outputfile.
+   */
+  static public $default_extension = 'txt';
+
+  /**
+   * The bundle name.
+   */
+  protected $bundle_name = '';
+
+  /**
+   * A set of entity IDs. The entities must all be of the same bundle type.
+   */
+  protected $entity_ids = array();
+
+  /**
+   * The set of fields
+   */
+  protected $fields = array();
+
+  /**
+   * The output file URI.
+   */
+  protected $outfile = '';
+
+  /**
+   * Constructs a new instance of the TripalFieldDownloader class.
+   * @param $bundle_name
+   *   The name of the bundle to which the IDs in the $id argument belong.
+   * @param $ids
+   *   An array of entity IDs.  The order of the IDs will be the order that
+   *   output is generated.
+   * @param $fields
+   *   An array of numeric field IDs to use when constructing the download. If
+   *   no fields are provided then all fields that are appropriate for the
+   *   given type will be used.
+   * @param $outfile_name
+   *   The name of the output file to create. The name should not include
+   *   a path.
+   */
+  public function __construct($bundle_name, $ids, $fields = array(),
+      $outfile_name, $uid) {
+
+    $user = user_load($uid);
+    if (!$user) {
+      throw new Exception(t("The provided user ID does not reference a real user: '@uid'.", array('@uid' => $uid)));
+    }
+    if (!$outfile_name) {
+      throw new Exception("Please provide an outputfilename");
+    }
+
+    $this->bundle_name = $bundle_name;
+    $this->entity_ids = $ids;
+    $this->fields = $fields;
+
+    // Make sure the user directory exists
+    $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;
+    }
+
+    $this->outfile = $user_dir. '/' . $outfile_name;
+  }
+
+  /**
+   * Retrieves the URL for the downloadable file.
+   */
+  public function getURL() {
+     return $this->outfile;
+  }
+
+  /**
+   * Removes the downloadable file.
+   */
+  public function delete() {
+    $fid = db_select('file_managed', 'fm')
+     ->fields('fm', array('fid'))
+     ->condition('uri', $this->outfile)
+     ->execute()
+     ->fetchField();
+     if ($fid) {
+       $file = file_load($fid);
+       file_usage_delete($file, 'tripal', 'data-collection');
+       file_delete($file, TRUE);
+     }
+  }
+
+  /**
+   * Creates the downloadable file.
+   */
+  public function write() {
+    global $user;
+
+    $fh = fopen(drupal_realpath($this->outfile), "w");
+    if (!$fh) {
+      throw new Exception("Cannout open collection file: " . $this->outfile);
+    }
+
+    $headers = $this->getHeader();
+    if ($headers) {
+      foreach ($headers as $line) {
+        fwrite($fh, $line . "\r\n");
+      }
+    }
+
+    foreach ($this->entity_ids as $entity_id) {
+      $result = tripal_load_entity('TripalEntity', array($entity_id), FALSE, $this->fields);
+      reset($result);
+      $entity = $result[$entity_id];
+      $lines = $this->formatEntity($entity);
+      foreach ($lines as $line) {
+        fwrite($fh, $line . "\r\n");
+      }
+    }
+    fclose($fh);
+
+    $file = new stdClass();
+    $file->uri = $this->outfile;
+    $file->filename = basename($this->outfile);
+    $file->filemime = file_get_mimetype($this->outfile);
+    $file->uid = $user->uid;
+    $file->status = FILE_STATUS_PERMANENT;
+    $file = file_save($file);
+    $fid = $file->fid;
+    $file = file_load($fid);
+    // We use the fid for the last argument because these files
+    // aren't really associated with any entity, but we need a value.
+    file_usage_add($file, 'tripal', 'data-collection', $fid);
+  }
+
+  /**
+   * Setups a download stream for the file.
+   */
+  public function download() {
+
+  }
+
+  /**
+   * Formats the entity and the specified fields for utput.
+   *
+   * This function should be implemented by a child class. It should iterate
+   * over the fields for the entity and return the appropriate format. It may
+   * return multiple lines of output if necessary.
+   *
+   * @param $entity
+   *   The entity object.  The fields that should be formatted are already
+   *   loaded.
+   *
+   * @return
+   *   An array of strings (one per line of output.
+   */
+  abstract protected function formatEntity($entity);
+
+  /**
+   *  Retreives header lines
+   *
+   *  This function should be implemented by a child class.  It should return
+   *  the header lines for an output file.
+   */
+  abstract protected function getHeader();
+}

+ 27 - 0
tripal/includes/TripalFieldDownloaders/TripalGFF3Downloader.inc

@@ -0,0 +1,27 @@
+<?php
+
+class TripalGFF3Downloader extends TripalFieldDownloader {
+  /**
+   * Sets the label shown to the user describing this formatter.
+   */
+  static public $label = 'GFF3';
+
+  /**
+   * Indicates the default extension for the outputfile.
+   */
+  static public $default_extension = 'gff3';
+
+  /**
+   * @see TripalFieldDownloader::format()
+   */
+  protected function formatEntity($entity) {
+
+  }
+
+  /**
+   * @see TripalFieldDownloader::getHeader()
+   */
+  protected function getHeader() {
+
+  }
+}

+ 115 - 0
tripal/includes/TripalFieldDownloaders/TripalNucFASTADownloader.inc

@@ -0,0 +1,115 @@
+<?php
+
+class TripalNucFASTADownloader extends TripalFieldDownloader {
+
+  /**
+   * Sets the label shown to the user describing this formatter.
+   */
+  static public $label = 'Nucleotide FASTA';
+
+  /**
+   * Indicates the default extension for the outputfile.
+   */
+  static public $default_extension = 'fna';
+
+  /**
+   * @see TripalFieldDownloader::format()
+   */
+  protected function formatEntity($entity) {
+    $lines = array();
+
+    // Get the list of all fields that have been attached to the entity
+    $instances = field_info_instances('TripalEntity', $entity->bundle);
+    $available_fields = array();
+    foreach ($instances as $field_name => $instance) {
+      if ($instance['field_name'] == 'entity_id') {
+        continue;
+      }
+      $available_fields[$instance['field_name']] = $instance;
+    }
+
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+
+      if (!property_exists($entity, $field_name)) {
+        continue;
+      }
+
+      // If we only have one element then this is good.
+      if (count($entity->{$field_name}['und']) == 1) {
+        $value = $entity->{$field_name}['und'][0]['value'];
+        // If the single element is not an array then this is good.
+        if (!is_array($value)) {
+
+          // We need to make sure we have some fields for the definition line.
+          // those may or may not have been included, so we should add them.
+          $defline = '>';
+          $found_identifier = FALSE;
+          if (property_exists($entity, 'data__identifier')) {
+            $found_identifier = TRUE;
+            $defline .= $entity->{'data__identifier'}['und'][0]['value'] . ' ';
+          }
+          if (property_exists($entity, 'schema__name')) {
+            $found_identifier = TRUE;
+            $defline .= $entity->{'schema__name'}['und'][0]['value'] . ' ';
+          }
+          if (property_exists($entity, 'data__accession')) {
+            $found_identifier = TRUE;
+            $defline .= $entity->{'data__accession'}['und'][0]['value'] . ' ';
+          }
+          if (!$found_identifier) {
+            $defline .= "Unknown feature identifier. Please add a name field to the data collection";
+          }
+          if (property_exists($entity, 'data__sequence_coordinates')) {
+            $location = strip_tags(drupal_render(field_view_field('TripalEntity', $entity, 'data__sequence_coordinates'))) . '; ';
+            $location = preg_replace('/\&nbsp\;/', ' ', $location);
+            $defline .= $location;
+          }
+          // Add to the defnition line values from any single valued fields.
+          foreach ($available_fields as $fname => $instance) {
+            if (count($entity->{$fname}['und']) == 1) {
+              if (!is_array($entity->{$fname}['und'][0]['value'])) {
+                // Skip the identifier fields and the residues fields.
+                if (!in_array($fname, array('data__identifier',
+                    'schema__name', 'data__protein_sequence', $field_name))) {
+                  $fvalue = $entity->{$fname}['und'][0]['value'];
+                  if ($fvalue) {
+                    $defline .= $instance['label'] . ': ' . $fvalue . '; ';
+                  }
+                }
+              }
+              else {
+                if (array_key_exists('rdfs:label', $entity->{$fname}['und'][0]['value'])) {
+                  $defline .= $instance['label'] . ': ' . strip_tags($entity->{$fname}['und'][0]['value']['rdfs:label']) . '; ';
+                }
+              }
+            }
+          }
+          $defline = rtrim($defline, '; ');
+
+          // Now add the residues.
+          $lines[] = $defline;
+          $residues = explode('|', wordwrap($value, 50, "|", TRUE));
+          foreach ($residues as $line) {
+            $lines[] = $line;
+          }
+        }
+        else {
+          // TODO: What to do with fields that are arrays?
+        }
+      }
+      else {
+        // TODO: What to do with fields that have multiple values?
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * @see TripalFieldDownloader::getHeader()
+   */
+  protected function getHeader() {
+
+  }
+}

+ 114 - 0
tripal/includes/TripalFieldDownloaders/TripalProteinFASTADownloader.inc

@@ -0,0 +1,114 @@
+<?php
+class TripalProteinFASTADownloader extends TripalFieldDownloader {
+
+  /**
+   * Sets the label shown to the user describing this formatter.
+   */
+  static public $label = 'Protein FASTA';
+
+  /**
+   * Indicates the default extension for the outputfile.
+   */
+  static public $default_extension = 'faa';
+
+  /**
+   * @see TripalFieldDownloader::format()
+   */
+  protected function formatEntity($entity) {
+    $lines = array();
+
+    // Get the list of all fields that have been attached to the entity
+    $instances = field_info_instances('TripalEntity', $entity->bundle);
+    $available_fields = array();
+    foreach ($instances as $field_name => $instance) {
+      if ($instance['field_name'] == 'entity_id') {
+        continue;
+      }
+      $available_fields[$instance['field_name']] = $instance;
+    }
+
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+
+      if (!property_exists($entity, $field_name)) {
+        continue;
+      }
+
+      // If we only have one element then this is good.
+      if (count($entity->{$field_name}['und']) == 1) {
+        $value = $entity->{$field_name}['und'][0]['value'];
+        // If the single element is not an array then this is good.
+        if (!is_array($value)) {
+
+          // We need to make sure we have some fields for the definition line.
+          // those may or may not have been included, so we should add them.
+          $defline = '>';
+          $found_identifier = FALSE;
+          if (property_exists($entity, 'data__identifier')) {
+            $found_identifier = TRUE;
+            $defline .= $entity->{'data__identifier'}['und'][0]['value'] . ' ';
+          }
+          if (property_exists($entity, 'schema__name')) {
+            $found_identifier = TRUE;
+            $defline .= $entity->{'schema__name'}['und'][0]['value'] . ' ';
+          }
+          if (property_exists($entity, 'data__accession')) {
+            $found_identifier = TRUE;
+            $defline .= $entity->{'data__accession'}['und'][0]['value'] . ' ';
+          }
+          if (!$found_identifier) {
+            $defline .= "Unknown feature identifier. Please add a name field to the data collection";
+          }
+          if (property_exists($entity, 'data__sequence_coordinates')) {
+            $location = strip_tags(drupal_render(field_view_field('TripalEntity', $entity, 'data__sequence_coordinates'))) . '; ';
+            $location = preg_replace('/\&nbsp\;/', ' ', $location);
+            $defline .= $location;
+          }
+          // Add to the defnition line values from any single valued fields.
+          foreach ($available_fields as $fname => $instance) {
+            if (count($entity->{$fname}['und']) == 1) {
+              if (!is_array($entity->{$fname}['und'][0]['value'])) {
+                // Skip the identifier fields and the residues fields.
+                if (!in_array($fname, array('data__identifier',
+                  'schema__name', 'data__sequence', $field_name))) {
+                  $fvalue = $entity->{$fname}['und'][0]['value'];
+                  if ($fvalue) {
+                    $defline .= $instance['label'] . ': ' . $fvalue . '; ';
+                  }
+                }
+              }
+              else {
+                if (array_key_exists('rdfs:label', $entity->{$fname}['und'][0]['value'])) {
+                  $defline .= $instance['label'] . ': ' . strip_tags($entity->{$fname}['und'][0]['value']['rdfs:label']) . '; ';
+                }
+              }
+            }
+          }
+          $defline = rtrim($defline, '; ');
+
+          // Now add the residues.
+          $lines[] = $defline;
+          $residues = explode('|', wordwrap($value, 50, "|", TRUE));
+          foreach ($residues as $line) {
+            $lines[] = $line;
+          }
+        }
+        else {
+          // TODO: What to do with fields that are arrays?
+        }
+      }
+      else {
+        // TODO: What to do with fields that have multiple values?
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * @see TripalFieldDownloader::getHeader()
+   */
+  protected function getHeader() {
+
+  }
+}

+ 65 - 0
tripal/includes/TripalFieldDownloaders/TripalTabDownloader.inc

@@ -0,0 +1,65 @@
+<?php
+
+class TripalTabDownloader extends TripalFieldDownloader {
+  /**
+   * Sets the label shown to the user describing this formatter.
+   */
+  static public $label = 'Tab delimeted';
+
+  /**
+   * Indicates the default extension for the outputfile.
+   */
+  static public $default_extension = 'txt';
+
+  /**
+   * @see TripalFieldDownloader::format()
+   */
+  protected function formatEntity($entity) {
+    $row = array();
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+
+      if (!property_exists($entity, $field_name)) {
+        continue;
+      }
+
+      // If we only have one element then this is good.
+      if (count($entity->{$field_name}['und']) == 1) {
+        $value = $entity->{$field_name}['und'][0]['value'];
+        // If the single element is not an array then this is good.
+        if (!is_array($value)) {
+          $row[] = $value;
+        }
+        else {
+          if (array_key_exists('rdfs:label', $entity->{$field_name}['und'][0]['value'])) {
+            $row[] = strip_tags($entity->{$field_name}['und'][0]['value']['rdfs:label']);
+          }
+          else {
+            $row[] = '';
+          }
+          // TODO: What to do with fields that are arrays?
+        }
+      }
+      else {
+        $row[] = '';
+        // TODO: What to do with fields that have multiple values?
+      }
+    }
+    return array(implode("\t", $row));
+  }
+
+  /**
+   * @see TripalFieldDownloader::getHeader()
+   */
+  protected function getHeader() {
+    $row = array();
+    foreach ($this->fields as $field_id) {
+      $field = field_info_field_by_id($field_id);
+      $field_name = $field['field_name'];
+      $instance = field_info_instance('TripalEntity', $field_name, $this->bundle_name);
+      $row[] = $instance['label'];
+    }
+    return array(implode("\t", $row));
+  }
+}

+ 7 - 5
tripal/includes/TripalFields/TripalField.inc

@@ -50,11 +50,12 @@ class TripalField {
     // Set to TRUE if the site admin is not allowed to change the term
     // type, otherwise the admin can change the term mapped to a field.
     'term_fixed' => FALSE,
-    // Inidates if this field should be automatically attached to display
-    // or web services or if this field should be loaded separately. This
-    // is convenient for speed.  Fields that are slow should for loading
-    // should ahve auto_attach set to FALSE so tha their values can be
-    // attached asyncronously.
+    // Indicates the download formats for this field.  The list must be the
+    // name of a child class of the TripalFieldDownloader.
+    'download_formatters' => array(
+      'TripalTabDownloader',
+      'TripalCSVDownloader',
+    ),
   );
 
   // The default widget for this field.
@@ -78,6 +79,7 @@ class TripalField {
   // the user but otherwise provides no data.
   public static $no_data = FALSE;
 
+
   // --------------------------------------------------------------------------
   //              PROTECTED CLASS MEMBERS -- DO NOT OVERRIDE
   // --------------------------------------------------------------------------

+ 0 - 5
tripal/includes/TripalFields/TripalFieldFormatter.inc

@@ -16,11 +16,6 @@ class TripalFieldFormatter {
    */
   public static $default_settings = array();
 
-  /**
-   * An array of formatter classes supported by the field.
-   */
-  public static $download_formats = array();
-
 
   /**
    * Instantiates a new TripalFieldFormatter object.

+ 65 - 0
tripal/includes/tripal.admin.inc

@@ -1,5 +1,70 @@
 <?php
 
+/**
+ *
+ * @param unknown $form
+ * @param unknown $form_state
+ */
+function tripal_admin_data_collection_form($form, &$form_state) {
+  $form = array();
+
+  $options = array(0 => t('Disabled'), 1 => t('Enabled'));
+  $form['enabled'] = array(
+    '#type' => 'radios',
+    '#title' => t('Activate Data Collections'),
+    '#options' => $options,
+    '#description' => t('Data collections allow users to save results of search queries for later download or use by other tools.'),
+    '#default_value' => variable_get('tripal_data_collections_enabled', 1),
+    '#required' => TRUE,
+  );
+
+  $form['lifespan'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Collection Lifespan'),
+    '#description' => t('Enter the number of hours (e.g. 24 is 1 day, 720 is 30 days)
+       that data collections exist.  Collections will be automatically removed after the lifespan
+       period has passed.  Removal of data collections occurs when the
+       sites Drupal cron executes.'),
+    '#default_value' => variable_get('tripal_data_collections_lifespan', 7),
+    '#required' => TRUE,
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save changes'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate function for the tripal_admin_data_collection_form form.
+ */
+function tripal_admin_data_collection_form_validate($form, &$form_state) {
+  $enable = $form_state['values']['enabled'];
+  $lifespan = $form_state['values']['lifespan'];
+
+  if (!preg_match('/^\d+$/',  $lifespan)) {
+    form_set_error('filespan', 'Please provide a valid numeric value for the number of hours collections exist.');
+  }
+  else {
+    if ($lifespan == 0) {
+      form_set_error('filespan', 'Please provide a positive numeric value for the number of hours collections exist.');
+    }
+  }
+}
+/**
+ * Sumbit function for the tripal_admin_data_collection_form form.
+ */
+function tripal_admin_data_collection_form_submit($form, &$form_state) {
+  $enabled = $form_state['values']['enabled'];
+  $lifespan = $form_state['values']['lifespan'];
+  variable_set('tripal_data_collections_enabled', $enabled);
+  variable_set('tripal_data_collections_lifespan', $lifespan);
+  menu_cache_clear_all();
+  drupal_set_message('Settings saved.');
+}
+
 /**
  * Provides a form for importing vocabularies and their terms.
  *

+ 114 - 0
tripal/includes/tripal.collections.inc

@@ -0,0 +1,114 @@
+<?php
+
+
+/**
+ *
+ */
+function tripal_user_collections_page() {
+  global $user;
+
+  $headers = array('Name', 'Description', 'Create Date', 'Downloads Formats', 'Actions');
+  $rows = array();
+
+  $collections = db_select('tripal_collection', 'tc')
+    ->fields('tc', array('collection_id'))
+    ->condition('uid', $user->uid)
+    ->orderBy('tc.collection_name')
+    ->execute();
+
+  while ($collection_id = $collections->fetchField()) {
+    $collection = new TripalEntityCollection();
+    $collection->load($collection_id);
+
+    $downloads = array();
+    $formatters = $collection->getDownloadFormatters();
+    foreach ($formatters as $class_name => $label) {
+      $outfile = $collection->getOutfilePath($class_name);
+      $outfileURL = file_create_url($outfile);
+      if (file_exists($outfile)) {
+        $downloads[] = l($label, $outfileURL);
+      }
+      else {
+        $downloads[] = 'Waiting on ' . $label . '...';
+      }
+    }
+    $download_list = theme_item_list(array(
+      'items' => $downloads,
+      'title' => '',
+      'type' => 'ul',
+      'attributes' => array(),
+    ));
+
+    $rows[] = array(
+      'data' => array(
+        $collection->getName(),
+        $collection->getDescription(),
+        $collection->getCreateDate(),
+        $download_list,
+        l('Delete', 'user/' . $user->uid . '/data-collections/' . $collection_id . '/delete'),
+      ),
+    );
+  }
+
+  $content['instructions'] = array(
+    '#type' => 'markup',
+    '#markup' => '<p>' .  t('Data collections allow you to store sets of data
+       for download or later use by other tools on this site.  Typically data
+       collections are created using search tools.  The following data
+       collections are available to you.  Some files take time to generate
+       before they can be downloaded.') . '</p>',
+  );
+  $content['files_table'] = array(
+    '#type' => 'item',
+    '#title' => 'Data Collections',
+    '#markup' => theme_table(array(
+      'header' => $headers,
+      'rows' => $rows,
+      'attributes' => array(),
+      'caption' => '',
+      'colgroups' => array(),
+      'sticky' => TRUE,
+      'empty' => t('You currently have no data collections.')
+    )),
+  );
+
+  return $content;
+}
+
+function tripal_user_collections_delete_form($form, &$form_state,  $uid, $collection_id) {
+  $form_state['collection_id'] = $collection_id;
+  $form['#submit'][] = 'tripal_user_collections_delete_form_submit';
+
+  $collection  = new TripalEntityCollection();
+  $collection->load($collection_id);
+
+  $form = confirm_form($form,
+      t('Click the delete button below to confirm deletion of the collection titled: %title',
+          array('%title' => $collection->getName())), 'user/' . $uid . '/data-collections',
+      '<p>' .t('This action cannot be undone.') .'</p>', t('Delete'), t('Cancel'), 'confirm');
+
+  return $form;
+}
+/**
+ * Deletes a user's collection.
+ *
+ * @param $collection_id
+ *   The ID of the collection to delete.
+ */
+function tripal_user_collections_delete_form_submit($form, &$form_state) {
+  global $user;
+  $collection_id = $form_state['collection_id'];
+  $collection  = new TripalEntityCollection();
+  $collection->load($collection_id);
+
+  if ($collection->getUserID() == $user->uid) {
+    try {
+      $collection->delete();
+      drupal_set_message('The data collection has been deleted.');
+    }
+    catch (Exception $e) {
+      drupal_set_message('There was a problem deleting the data collection please contact the site to report the error.', 'error');
+    }
+  }
+  drupal_goto('user/' . $user->uid . '/data-collections');
+}

+ 12 - 3
tripal/includes/tripal.fields.inc

@@ -34,10 +34,10 @@ function tripal_field_info() {
  * Implements hook_info_alter().
  */
 function tripal_field_info_alter(&$info) {
-  // Make sure all fields have a term setting so we can map
-  // all fields to a vocabulary term for the semantic web.
   foreach ($info as $field_name => $details) {
-    if(array_key_exists('instance_settings', $details)) {
+    // Make sure all fields have a term setting so we can map
+    // all fields to a vocabulary term for the semantic web.
+    if (array_key_exists('instance_settings', $details)) {
       if (!array_key_exists('term_vocabulary', $details['instance_settings'])) {
         $info[$field_name]['instance_settings']['term_vocabulary'] = '';
       }
@@ -61,6 +61,15 @@ function tripal_field_info_alter(&$info) {
       $info[$field_name]['instance_settings']['term_fixed'] = FALSE;
       $info[$field_name]['instance_settings']['auto_attach'] = TRUE;
     }
+
+    // Make sure all fields have some sort of downloader support. The Tab
+    // delimited and CSV downloaders should support all types of fields.
+    if (!array_key_exists('download_formatters', $details)) {
+      $info[$field_name]['instance_settings']['download_formatters'] = array(
+        'TripalTabDownloader',
+        'TripalCSVDownloader',
+      );
+    }
   }
 }
 

+ 3 - 2
tripal/theme/js/TripalUploader.js

@@ -127,8 +127,9 @@
       var url = options['url'];
       var self = this;
       
-      // Make sure the file type is allowed
-      if (this.tables[tname]['allowed_types']) {
+      // Make sure the file type is allowed.  If there are no file types
+      // then anything is allowed.
+      if (this.tables[tname]['allowed_types'] && this.tables[tname]['allowed_types'].length > 0) {
         var allowed_types = this.tables[tname]['allowed_types'];
         var matches = file.name.match(/^.*\.(.+)$/);
         if (!matches) {

+ 1 - 0
tripal/tripal.info

@@ -9,6 +9,7 @@ configure = admin/tripal
 stylesheets[all][] = theme/css/tripal.css
 scripts[]          = theme/js/tripal.js
 
+files[] = views_handlers/tripal_views_handler_area_collections.inc
 files[] = views_handlers/tripal_views_handler_field.inc
 files[] = views_handlers/tripal_views_handler_field_element.inc
 files[] = views_handlers/tripal_views_handler_field_entity.inc

+ 96 - 20
tripal/tripal.install

@@ -16,7 +16,7 @@ function tripal_install() {
     db_add_field('tripal_jobs', 'includes', array(
       'type' => 'text',
       'description' => 'A serialized array of file paths that should be included prior to executing the job.',
-      'not NULL' => FALSE,
+      'not null' => FALSE,
     ));
   }
 }
@@ -193,6 +193,7 @@ function tripal_schema() {
   $schema['tripal_entity'] = tripal_tripal_entity_schema();
   $schema['tripal_bundle'] = tripal_tripal_bundle_schema();
   $schema['tripal_import'] = tripal_tripal_import_schema();
+  $schema['tripal_collection'] = tripal_tripal_collection_schema();
 
   // Adds a table for additional information related to bundles.
   $schema['tripal_bundle_variables'] = tripal_tripal_bundle_variables_schema();
@@ -209,96 +210,96 @@ function tripal_tripal_jobs_schema() {
       'job_id' => array(
         'type' => 'serial',
         'unsigned' => TRUE,
-        'not NULL' => TRUE
+        'not null' => TRUE
       ),
       'uid' => array(
         'type' => 'int',
         'unsigned' => TRUE,
-        'not NULL' => TRUE,
+        'not null' => TRUE,
         'description' => 'The Drupal userid of the submitee'
       ),
       'job_name' => array(
         'type' => 'varchar',
         'length' => 255,
-        'not NULL' => TRUE
+        'not null' => TRUE
       ),
       'modulename' => array(
         'type' => 'varchar',
         'length' => 50,
-        'not NULL' => TRUE,
+        'not null' => TRUE,
         'description' => 'The module name that provides the callback for this job'
       ),
       'callback' => array(
         'type' => 'varchar',
         'length' => 255,
-        'not NULL' => TRUE
+        'not null' => TRUE
       ),
       'arguments' => array(
         'type' => 'text',
         'size' => 'normal',
-        'not NULL' => FALSE
+        'not null' => FALSE
       ),
       'progress' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'default' => 0,
-        'not NULL' => FALSE,
+        'not null' => FALSE,
         'description' => 'a value from 0 to 100 indicating percent complete'
       ),
       'status' => array(
         'type' => 'varchar',
         'length' => 50,
-        'not NULL' => TRUE
+        'not null' => TRUE
       ),
       'submit_date' => array(
         'type' => 'int',
-        'not NULL' => TRUE,
+        'not null' => TRUE,
         'description' => 'UNIX integer submit time'
       ),
       'start_time' => array(
         'type' => 'int',
-        'not NULL' => FALSE,
+        'not null' => FALSE,
         'description' => 'UNIX integer start time'
       ),
       'end_time' => array(
         'type' => 'int',
-        'not NULL' => FALSE,
+        'not null' => FALSE,
         'description' => 'UNIX integer end time'
       ),
       'error_msg' => array(
         'type' => 'text',
         'size' => 'normal',
-        'not NULL' => FALSE
+        'not null' => FALSE
       ),
       'pid' => array(
         'type' => 'int',
         'unsigned' => TRUE,
-        'not NULL' => FALSE,
+        'not null' => FALSE,
         'description' => 'The process id for the job'
       ),
       'priority' => array(
         'type' => 'int',
         'unsigned' => TRUE,
-        'not NULL' => TRUE,
+        'not null' => TRUE,
         'default' => '0',
         'description' => 'The job priority'
       ),
       'mlock' => array(
         'type' => 'int',
         'unsigned' => TRUE,
-        'not NULL' => FALSE,
+        'not null' => FALSE,
         'description' => 'If set to 1 then all jobs for the module are held until this one finishes'
       ),
       'lock' => array(
         'type' => 'int',
         'unsigned' => TRUE,
-        'not NULL' => FALSE,
+        'not null' => FALSE,
         'description' => 'If set to 1 then all jobs are held until this one finishes'
       ),
       'includes' => array(
         'type' => 'text',
         'description' => 'A serialized array of file paths that should be included prior to executing the job.',
-        'not NULL' => FALSE,
+        'not null' => FALSE,
       )
     ),
     'indexes' => array(
@@ -308,6 +309,64 @@ function tripal_tripal_jobs_schema() {
     'primary key' => array('job_id'),
   );
 }
+/**
+ * Returns the Drupal Schema API array for the tripal_jobs table.
+ */
+function tripal_tripal_collection_schema() {
+  return array(
+    'fields' => array(
+      'collection_id' => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE
+      ),
+      'collection_name' => array(
+        'type' => 'varchar',
+        'length' => 1024,
+        'not null' => TRUE
+      ),
+      'description' => array(
+        'type' => 'text',
+        'not null' => FALSE
+      ),
+      'bundle_name' => array(
+        'type' => 'varchar',
+        'length' => 1024,
+        'not null' => TRUE
+      ),
+      'ids' => array(
+        'type' => 'text',
+        'size' => 'normal',
+        'not null' => TRUE,
+        'description' => 'An array of entity IDs.'
+      ),
+      'fields' => array(
+        'type' => 'text',
+        'size' => 'normal',
+        'not null' => TRUE,
+        'description' => 'An array of numeric field IDs.'
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'description' => 'The user Id of the person who created the collection.'
+      ),
+      'create_date' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'description' => 'UNIX integer start time'
+      ),
+    ),
+    'indexes' => array(
+      'bundle_name' => array('bundle_name'),
+      'uid' => array('uid')
+    ),
+    'unique keys' => array(
+      'user_collection' => array('uid', 'collection_name'),
+    ),
+    'primary key' => array('collection_id'),
+  );
+}
 /**
  * Returns the Drupal Schema API array for the tripal_jobs table.
  */
@@ -914,7 +973,7 @@ function tripal_update_7304() {
   catch (\PDOException $e) {
     $transaction->rollback();
     $error = $e->getMessage();
-    throw new DrupalUpdateException('Could not perform update: '. $error);
+    throw new DrupalUpdateException('Could not perform update: ' . $error);
   }
 }
 
@@ -933,6 +992,23 @@ function tripal_update_7305() {
   catch (\PDOException $e) {
     $transaction->rollback();
     $error = $e->getMessage();
-    throw new DrupalUpdateException('Could not perform update: '. $error);
+    throw new DrupalUpdateException('Could not perform update: ' . $error);
+  }
+}
+
+/**
+ * Adds the tripal_collection table.
+ */
+function tripal_update_7306() {
+  $transaction = db_transaction();
+  try {
+    $schema = array();
+    $schema['tripal_collection'] = tripal_tripal_collection_schema();
+    db_create_table('tripal_collection', $schema['tripal_collection']);
+  }
+  catch (\PDOException $e) {
+    $transaction->rollback();
+    $error = $e->getMessage();
+    throw new DrupalUpdateException('Could not add the tripal_collection table:' . $error);
   }
 }

+ 87 - 3
tripal/tripal.module

@@ -31,6 +31,8 @@ require_once "includes/TripalFields/TripalFieldFormatter.inc";
 require_once "includes/TripalFieldQuery.inc";
 require_once "includes/TripalJob.inc";
 require_once "includes/TripalImporter.inc";
+require_once "includes/TripalEntityCollection.inc";
+require_once "includes/TripalFieldDownloaders/TripalFieldDownloader.inc";
 
 /**
  * @defgroup tripal Tripal Core Module
@@ -97,6 +99,9 @@ function tripal_menu() {
     'file path' => drupal_get_path('module', 'system'),
   );
 
+  /**
+   * Tripal Extensions
+   */
   $items['admin/tripal/storage'] = array(
     'title' => 'Data Storage',
     'description' => t("Tripal is designed to access biological
@@ -145,7 +150,9 @@ function tripal_menu() {
 //     'weight' => -100,
 //   );
 
-  // Jobs Management
+  /**
+   * Jobs Management
+   */
   $items['admin/tripal/tripal_jobs'] = array(
     'title' => 'Jobs',
     'description' => t('Provides tools for managing jobs submitted to Tripal.  In some
@@ -329,9 +336,79 @@ function tripal_menu() {
     }
   }
 
+  /**
+   * Data Collections
+   */
+  $items['user/%/data-collections'] = array (
+    'title' => 'Data Collections',
+    'description' => 'Your list of saved data collections',
+    'page callback' => 'tripal_user_collections_page',
+    'access callback' => 'tripal_accesss_user_collections',
+    'access arguments' => array(1),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'includes/tripal.collections.inc',
+    'file path' => drupal_get_path('module', 'tripal'),
+  );
+
+  $items['user/%/data-collections/%/delete'] = array (
+    'title' => 'Delete a Collections',
+    'description' => 'Deletes a data collection.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('tripal_user_collections_delete_form', 1, 3),
+    'access callback' => 'tripal_accesss_user_collections',
+    'access arguments' => array(1),
+    'type' => MENU_CALLBACK,
+    'file' => 'includes/tripal.collections.inc',
+    'file path' => drupal_get_path('module', 'tripal'),
+  );
+
+  $items['admin/tripal/data-collections'] = array(
+    'title' => 'Data Collections',
+    'description' => t('Site-wide settings for data collections'),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('tripal_admin_data_collection_form'),
+    'access arguments' => array('administer tripal'),
+    'type' => MENU_NORMAL_ITEM,
+    'weight' => 0,
+    'file' => 'includes/tripal.admin.inc',
+    'file path' => drupal_get_path('module', 'tripal'),
+  );
+
   return $items;
 }
 
+function tripal_accesss_user_collections($uid) {
+  if (!tripal_access_user_data($uid)) {
+    return FALSE;
+  }
+  $collections_enabled = variable_get('tripal_data_collections_enabled', 1);
+  if (!$collections_enabled) {
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Access callback for accessing a user's Tripal-added private data.
+ *
+ * The User account being accessed must match the ID of the current user. This
+ * function can be used to check access for any type of user-specfic data
+ * added by any Tripal module.
+ *
+ * @param  $uid
+ *   The UID of the user's account to access.
+ * @return boolean
+ */
+function tripal_access_user_data($uid) {
+  global $user;
+
+  drupal_debug($uid);
+  if ($uid == $user->uid) {
+    return TRUE;
+  }
+  return FALSE;
+}
+
 /**
  * Implements hook_permission().
  */
@@ -626,6 +703,7 @@ function tripal_import_api() {
   module_load_include('inc', 'tripal', 'api/tripal.notice.api');
   module_load_include('inc', 'tripal', 'api/tripal.variables.api');
   module_load_include('inc', 'tripal', 'api/tripal.upload.api');
+  module_load_include('inc', 'tripal', 'api/tripal.collections.api');
   module_load_include('inc', 'tripal', 'api/tripal.DEPRECATED.api');
 }
 
@@ -928,14 +1006,20 @@ function tripal_block_configure($delta = '') {
  * Implements hook_cron().
  */
 function tripal_cron() {
+
+  // Add jobs to the Tripal queue for commong tasks.
+  $args = array();
+
   if (variable_get('tripal_admin_notification_creation_during_cron', TRUE)) {
     $modules = module_implements('tripal_cron_notification');
     foreach ($modules as $module) {
       $function = $module . '_tripal_cron_notification';
-      $function();
+      tripal_add_job("Cron: Checking for '$module' notifications.", 'tripal', $function, $args, 1);
     }
-    watchdog('tripal_cron', 'tripal_cron ran');
   }
+
+  // Check for expired collections.
+  tripal_add_job('Cron: Checking expired collections', 'tripal', 'tripal_expire_collections', $args, 1);
 }
 
 /**

+ 7 - 0
tripal/tripal.views.inc

@@ -33,6 +33,13 @@ function tripal_views_data() {
   tripal_views_data_tripal_entity($data);
   tripal_views_data_fields($data);
 
+  $data['views']['tripal_area_collections'] = array(
+    'title' => t('Tripal Content Data Collections'),
+    'help' => t('Save Tripal content search results into a data collection for downloading or use with other tools.'),
+    'area' => array(
+      'handler' => 'tripal_views_handler_area_collections',
+    ),
+  );
   return $data;
 }
 

+ 6 - 1
tripal/tripal.views_default.inc

@@ -63,7 +63,7 @@ function tripal_bundle_default_views(&$views) {
     $handler->display->display_options['query']['type'] = 'views_query';
     $handler->display->display_options['exposed_form']['type'] = 'basic';
     $handler->display->display_options['pager']['type'] = 'full';
-    $handler->display->display_options['pager']['options']['items_per_page'] = '25';
+    $handler->display->display_options['pager']['options']['items_per_page'] = '10';
     $handler->display->display_options['pager']['options']['offset'] = '0';
     $handler->display->display_options['pager']['options']['id'] = '0';
     $handler->display->display_options['pager']['options']['quantity'] = '9';
@@ -208,6 +208,11 @@ function tripal_bundle_default_views(&$views) {
       $handler->display->display_options['sorts']['priority']['field'] = 'taxrank__species';
     }
 
+    /* Footer: Global: Tripal Content Data Collections */
+    $handler->display->display_options['footer']['tripal_area_collections']['id'] = 'tripal_area_collections';
+    $handler->display->display_options['footer']['tripal_area_collections']['table'] = 'views';
+    $handler->display->display_options['footer']['tripal_area_collections']['field'] = 'tripal_area_collections';
+
     // No results behavior: Global: Text area.
     $handler->display->display_options['empty']['text']['id'] = 'text';
     $handler->display->display_options['empty']['text']['table'] = 'views';

+ 221 - 0
tripal/views_handlers/tripal_views_handler_area_collections.inc

@@ -0,0 +1,221 @@
+<?php
+
+class tripal_views_handler_area_collections extends views_handler_area_result {
+
+  function options_form(&$form, &$form_state) {
+    // We have no options so we have to implement this function with
+    // nothing in it.
+  }
+  /**
+   * Implements views_handler_area_result::render().
+   */
+  function render($empty = FALSE) {
+
+    // If collections are dispabled then don't show anything.
+    $collections_enabled = variable_get('tripal_data_collections_enabled', 1);
+    if (!$collections_enabled) {
+      return '';
+    }
+
+    // This will only work with Tripal content types and the tripal_views_query
+    // plugin. So don't show anything for others.
+    if ($this->query->plugin_name != 'tripal_views_query') {
+      return '';
+    }
+    $form = drupal_get_form('tripal_views_handler_area_collections_form', $this->view, $this->query);
+    return drupal_render($form);
+  }
+}
+
+/**
+ *
+ */
+function tripal_views_handler_area_collections_form($form, $form_state, $view, $query) {
+
+  // Set form defaults.
+  $collection_name = '';
+  $collection_desc = '';
+
+  // Get the bundle for this query.
+  $matches = array();
+  preg_match('/^(.+?)__(.+?)$/', $view->base_table, $matches);
+  $vocabulary = $matches[1];
+  $accession = $matches[2];
+  $term = tripal_load_term_entity(array('vocabulary' => $vocabulary, 'accession' => $accession));
+  $bundle = tripal_load_bundle_entity(array('term_id' => $term->id));
+
+  $form = array();
+  $form['save_collection'] = array(
+   '#type' => 'fieldset',
+   '#title' => t('Save Results'),
+   '#collapsible' => TRUE,
+   '#collapsed' => TRUE,
+   '#description' => t('A data collection is a virtual container into which you can
+     save data.  You can place your search results into a data collection for
+     download or use with other tools on this site that support data collections.'),
+  );
+  $form['save_collection']['bundle'] = array(
+    '#type' => 'value',
+    '#value' => $bundle,
+  );
+  $form['save_collection']['view'] = array(
+    '#type' => 'value',
+    '#value' => unserialize(serialize($view))
+  );
+  $form['save_collection']['query'] = array(
+    '#type' => 'value',
+    '#value' => unserialize(serialize($query->query))
+  );
+  $form['save_collection']['summary'] = array(
+   '#type' => 'item',
+   '#title' => 'Results Summary',
+   '#markup' => t('There are @total_rows record(s) that can be added to a data collection.', array('@total_rows' => $view->total_rows)),
+  );
+  $form['save_collection']['collection_name'] = array(
+   '#type' => 'textfield',
+   '#title' => t('Collection Name'),
+   '#description' => t('Please name this collection for future reference.'),
+   '#default_value' => $collection_name,
+   '#required' => TRUE,
+  );
+  $form['save_collection']['collection_desc'] = array(
+   '#type' => 'textarea',
+   '#title' => t('Description'),
+   '#description' => t('Please provide a description about this data collection.'),
+   '#default_value' => $collection_name,
+  );
+
+  // Get the list of fields used in the view.
+  $current_display = $view->current_display;
+  if (array_key_exists('fields', $view->display[$current_display]->display_options)) {
+    $view_fields = $view->display[$current_display]->display_options['fields'];
+  }
+  else {
+    $view_fields = $view->display['default']->display_options['fields'];
+  }
+  // Get the list of fields in this view.
+  $field_ids = array();
+  $defaults = array();
+  $fields = field_info_instances('TripalEntity', $bundle->name);
+  foreach ($fields as $field_name => $instance) {
+    $field = field_info_field($field_name);
+    $field_type = $field['type'];
+    if ($instance['field_name'] == 'entity_id') {
+      continue;
+    }
+    // Skip hidden fields.
+    if ($instance['display']['default']['type'] == 'hidden') {
+      continue;
+    }
+    $field_label = $instance['label'];
+
+    // Add in in any non CSV or Tab formatters to the label.
+    $formatters = array();
+    if (tripal_load_include_field_class($field_type)) {
+      $instance_settings = $field_type::$default_instance_settings;
+      if (array_key_exists('download_formatters', $instance_settings)) {
+        foreach ($instance_settings['download_formatters'] as $class_name) {
+          if ($class_name != 'TripalTabDownloader' and $class_name != 'TripalCSVDownloader') {
+            tripal_load_include_downloader_class($class_name);
+              $formatters[] = $class_name::$label;
+          }
+        }
+      }
+    }
+    if (count($formatters) > 0) {
+      $field_label .= ' (' . implode(',' , $formatters) . ')';
+    }
+
+    // Add the field to those supported.
+    $field_ids[$instance['field_id']] =  $field_label;
+
+    // Automatically check fields that are in the view and not excluded.
+    if (array_key_exists($field_name, $view_fields)) {
+      if (array_key_exists('exclude', $view_fields[$field_name]) and
+          $view_fields[$field_name]['exclude'] == TRUE) {
+        continue;
+      }
+      $defaults[] = $instance['field_id'];
+    }
+  }
+  $form['save_collection']['fields'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Advanced field selection'),
+    '#description' => t('Please select the fields to include in this data
+      collection. Not all of these fields appear in the search results
+      above but they are available for this content type. By default,
+      tab-delimeted and comma-separated files are genearted for the
+      collection using only the fields selected. However, some fields when
+      selected will generate other downloadable file formats.  Fields that
+      generate other file formats are indicated. '),
+    '#collapsible' => TRUE,
+    '#collapsed' => TRUE,
+  );
+  $form['save_collection']['fields']['field_ids'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Field Selection'),
+    '#options' => $field_ids,
+    '#default_value' => $defaults,
+  );
+
+  $form['save_collection']['button'] = array(
+    '#type' => 'submit',
+    '#value' => 'Save Data Collection',
+    '#name' => 'save_collection',
+    '#ajax' => array(
+      'callback' => "tripal_views_handler_area_collections_form_ajax",
+      'wrapper' => 'tripal-views-handler-area-collections',
+      'effect'   => 'fade',
+      'method'   => 'replace',
+      'prevent'  => 'click'
+    ),
+  );
+  $form['#prefix'] = '<div id="tripal-views-handler-area-collections">';
+  $form['#suffix'] = '</div>';
+  return $form;
+}
+
+/**
+ *
+ */
+function tripal_views_handler_area_collections_form_ajax($form, $form_state) {
+  return $form;
+}
+/**
+ *
+ */
+function tripal_views_handler_area_collections_form_submit($form, $form_state) {
+  global $user;
+
+  $bundle = $form_state['values']['bundle'];
+  $view = $form_state['values']['view'];
+  $query = $form_state['values']['query'];
+  $collection_name = trim($form_state['values']['collection_name']);
+  $description = $form_state['values']['collection_desc'];
+  $field_ids = array_key_exists('field_ids', $form_state['values']) ? $form_state['values']['field_ids'] : array();
+  $uid = $user->uid;
+  $bundle_name = $bundle->name;
+
+  $selected_fids = array();
+  foreach ($field_ids as $field_id => $is_selected) {
+    if ($is_selected) {
+      $selected_fids[] = $field_id;
+    }
+  }
+
+  // Get the entity Ids that match results
+  $query->range['length'] = $view->total_rows;
+  $results = $query->execute();
+  $entities = array();
+  foreach ($results['TripalEntity'] as $entity) {
+    $entities[] = $entity->id;
+  }
+  $collection = tripal_create_collection(array(
+    'uid'  => $uid,
+    'collection_name' => $collection_name,
+    'bundle_name' => $bundle_name,
+    'ids' => $entities,
+    'fields' => $selected_fids,
+    'description'  => $description,
+  ));
+}

+ 1 - 1
tripal_bulk_loader/tripal_bulk_loader.module

@@ -66,7 +66,7 @@ function tripal_bulk_loader_menu() {
 
   // Admin pages -----------------
   $items['admin/tripal/loaders/bulk'] = array(
-    'title' => 'Bulk Loader',
+    'title' => 'Chado Bulk Loader',
     'description' => 'Templates for loading tab-delimited data',
     'page callback' => 'tripal_bulk_loader_admin_jobs_listing',
     'access arguments' => array('administer tripal_bulk_loader'),

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

@@ -225,6 +225,7 @@ function tripal_get_chado_semweb_column($chado_table, $term) {
     ->fields('CS')
     ->condition('chado_table', $chado_table)
     ->execute();
+
   while($column = $columns->fetchObject()) {
     $cvterm_id = $column->cvterm_id;
 

+ 6 - 0
tripal_chado/includes/TripalFields/ChadoField.inc

@@ -41,6 +41,12 @@ class ChadoField extends TripalField {
     'chado_column' => '',
     // The base table.
     'base_table' => '',
+    // Indicates the download formats for this field.  The list must be the
+    // name of a child class of the TripalFieldDownloader.
+    'download_formatters' => array(
+      'TripalTabDownloader',
+      'TripalCSVDownloader',
+    ),
   );
 
   // The module that manages this field.

+ 7 - 0
tripal_chado/includes/TripalFields/data__protein_sequence/data__protein_sequence.inc

@@ -35,6 +35,13 @@ class data__protein_sequence extends ChadoField {
     // type. This will create form elements when editing the field instance
     // to allow the site admin to change the term settings above.
     'term_fixed' => FALSE,
+    // Indicates the download formats for this field.  The list must be the
+    // name of a child class of the TripalFieldDownloader.
+    'download_formatters' => array(
+      'TripalTabDownloader',
+      'TripalCSVDownloader',
+      'TripalProteinFASTADownloader',
+    ),
   );
 
   // The default widget for this field.

+ 7 - 0
tripal_chado/includes/TripalFields/data__sequence/data__sequence.inc

@@ -35,6 +35,13 @@ class data__sequence extends ChadoField {
     // type. This will create form elements when editing the field instance
     // to allow the site admin to change the term settings above.
     'term_fixed' => FALSE,
+    // Indicates the download formats for this field.  The list must be the
+    // name of a child class of the TripalFieldDownloader.
+    'download_formatters' => array(
+      'TripalTabDownloader',
+      'TripalCSVDownloader',
+      'TripalNucFASTADownloader',
+    ),
   );
 
   // The default widget for this field.