modules/custom/deims_data_explorer/deims_data_explorer.module

Summary

Maintainability
Test Coverage
<?php

/**
 * @file
 * DEIMS data explorer allows users to view and download remote data sources.
 *
 * @todo Move all non-hooks and page callbacks out of the module file
 * @todo Use variable_get() and not constants
 * @todo Add a 'Use the data explorer' permission
 */

/**
 * Define the maximum number of rows to display in the data preview.
 */
define("DEIMS_PREVIEW_COUNT", variable_get('deims_preview_count', 500));

/**
 * Define the maximum number of rows to be downloaded.
 */
define("DEIMS_DOWNLOAD_COUNT", variable_get('deims_download_count', 999999));

/**
 * Define the maximum number of rows be downloaded.
 */
define("DEIMS_BATCH_LIMIT", variable_get('deims_batch_limit', 25000));

/**
 * Define the smallest size file that will be compressed (in MB).
 */
define("DEIMS_COMPRESSION_THRESHOLD", variable_get('deims_compression_threshold', 10));

/**
 * The field machine name created in deims_install().
 */
define('DEIMS_DATA_EXPLORER_FIELD', 'field_deims_data_explorer');

/**
 * Implements hook_help().
 */
function deims_data_explorer_help($path, $arg) {

  switch ($path) {
    case 'admin/help#deims_data_explorer';
      return '<h3>' . t('About') . '</h3>' . '<p>' . t("The DEIMS Data Explorer enables you to make queries against
      tables in external databases.  In order to use it, you need to place a database connectors to your external database
      in a file called settings.local.php, which will also be at sites/default, and will have a similar \$database() array.
      For more extensive help on how to configure it, please visit the <a href=\"http://drupal.org/node/2141695\">
      DEIMS Data Explorer config help page.</a>
      Once the Data Explorer is set up, you will be able to connect your data source nodes to the corresponding table in the
      connected external database, right at the bottom of the data source form.  Each variable of the data-source is
      configurable: you may choose to allow user to query and filter on its values.  Once you have configured the data-source
      settings, you may access the data explorer page at the path node/%nid/data or using the Data Explorer Dashboard link
      in the menu (if active). More info on Data Explorer usage at <a href=\"http://drupal.org/node/2141613\">
      the DEIMS Data Explorer usage help page.</a>") . '</p>';

    case 'node/%/data':
      break;

  }
}

/**
 * Implements hook_menu().
 */
function deims_data_explorer_menu() {
  $items = array();
  $items['node/%node/data'] = array(
    'page callback' => 'deims_data_explorer_page',
    'page arguments' => array(1),
    'access callback'  => 'deims_data_explorer_page_access',
    'access arguments' => array(1),
    'type' => MENU_CALLBACK,
    'file' => 'deims_data_explorer.pages.inc',
  );
  $items['node/%node/data/download/%file'] = array(
    'page callback' => 'deims_data_explorer_download',
    'page arguments' => array(1, 4),
    'access callback' => 'deims_data_explorer_download_access',
    'access arguments' => array(1, 4),
    'type' => MENU_CALLBACK,
    'file' => 'deims_data_explorer.pages.inc',
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function deims_data_explorer_permission() {
  $info = array();

  // This permission doesn't actually do anything because there are no
  // administration pages. So it has been commented out for now.
  // $info['administer DEIMS data explorer'] = array(
  //  'title' => t('Administer DEIMS data explorer'),
  //  'restrict access' => TRUE,
  //);

  // $info['use DEIMS data explorer'] = array(
  //  'title' => t('Use the DEIMS data explorer'),
  //);

  return $info;
}

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter().
 */
function deims_data_explorer_field_widget_deims_variable_default_form_alter(&$element, &$form_state, $context) {
  $item = isset($context['items'][$context['delta']]) ? $context['items'][$context['delta']] : array();

  $element['data']['data_explorer_settings'] = array(
    '#type' => 'checkboxes',
    '#title' => 'Data Explorer Settings',
    '#options' => array(
      'expose' => t('Expose this variable'),
      'filter' => t('Allow users to filter on this variable'),
    ),
    '#default_value' => isset($item['data']['data_explorer_settings']) ? $item['data']['data_explorer_settings'] : array(),
    '#weight' => '100',
  );
}

/**
 * Implements hook_node_load().
 */
function deims_data_explorer_node_load(array $nodes) {
  foreach ($nodes as $node) {
    if ($node->type == 'data_source' && $query = deims_data_explorer_get_query_from_data_source($node)) {
      $count = $query->countQuery()->execute()->fetchField();
      // Store the row count of the external database in the field value so
      // it can be checked later.
      $langcode = field_language('node', $node, DEIMS_DATA_EXPLORER_FIELD);
      $node->{DEIMS_DATA_EXPLORER_FIELD}[$langcode][0]['row_count'] = $count;
    }
  }
}

/**
 * Implements hook_views_api().
 */
function deims_data_explorer_views_api() {
  return array('api' => 3.0);
}

/**
 * Implements hook_views_default_views().
 */
function deims_data_explorer_views_default_views() {
  // Check for all view file in views directory.
  $files = file_scan_directory(drupal_get_path('module', 'deims_data_explorer') . '/views', '/.*\.inc$/');

  // Add view to list of views.
  foreach ($files as $filepath => $file) {  
    require $filepath;
    if (isset($view)) {
      $views[$view->name] = $view;
    }
  }

  // At the end, return array of default views.
  return $views;
}

/**
 * Access callback for the data explorer page.
 *
 * @param object $node
 *   The data source node object.
 */
function deims_data_explorer_page_access($node) {
  return $node->type == 'data_source'
      && node_access('view', $node)
      && ($connection = field_get_items('node', $node, DEIMS_DATA_EXPLORER_FIELD))
      && !empty($connection[0]['row_count'])
      && ($variables = field_get_items('node', $node, 'field_variables'))
      && array_filter($variables, 'deims_data_explorer_filter_allowed_column_variable');
}

/**
 * Access callback for the data explorer download file page.
 *
 * @param object $node
 *   The data source node object.
 * @param object $file
 *   The exported CSV file object.
 */
function deims_data_explorer_download_access($node, $file) {
  return deims_data_explorer_page_access($node) && file_entity_access('download', $file);
}

/**
 * Implements hook_file_entity_access().
 *
 * By default temporary files are not downloadable. Because data explorer
 * exported files are saved as temporary, we need to manually allow those
 * files to be downloaded.
 */
function deims_data_explorer_file_entity_access($operation, $file, $account) {
  if ($operation == 'download' && $file->status != FILE_STATUS_PERMANENT && $file->uid == $account->uid && strpos($file->uri, variable_get('deims_data_explorer_dir', 'public://data-downloads/')) !== FALSE) {
    return FILE_ENTITY_ACCESS_ALLOW;
  }
  return FILE_ENTITY_ACCESS_IGNORE;
}

/**
 * Implements hook_field_attach_validate().
 *
 * Add extra validation if the user picks a database, but not a table or view.
 */
function deims_data_explorer_field_attach_validate($entity_type, $entity, array &$errors) {
  if ($entity_type == 'node' && $entity->type == 'data_source' && $items = field_get_items($entity_type, $entity, DEIMS_DATA_EXPLORER_FIELD)) {
    if (!empty($items[0]['connection']) && empty($items[0]['table'])) {
      $errors[DEIMS_DATA_EXPLORER_FIELD][LANGUAGE_NONE][0][] = array(
        'error' => 'field_missing_table',
        'message' => t('Cannot select a connection without selecting a table or view as well.'),
        'element' => array('table'),
      );
    }
  }
}

/**
 * Implements hook_workbench_block().
 *
 * Show a message in the workbench block if the data source has a valid
 * connection/table for the data explorer, but the data explorer doesn't have
 * any records.
 */
function deims_data_explorer_workbench_block() {
  $data_explorer_problems = array();

  // Add editing information to this page (if it's a node).
  if (($node = menu_get_object()) && $node->type == 'data_source' && node_access('update', $node)) {
    // @todo Add more helpful validation for conditions in deims_data_explorer_page_access() that could fail.
    if ($items = field_get_items('node', $node, DEIMS_DATA_EXPLORER_FIELD)) {
      if (empty($items[0]['row_count'])) {
        $data_explorer_problems[] = t('The table or view %table in the %connection database does not have any records. The data explorer will not be available until there is at least one record and caches are cleared.', array('%table' => $items[0]['table'], '%connection' => $items[0]['connection']));
      }
    }
    else {
      $data_explorer_problems[] = t('No table or view selected to query from.');
    }
    if ($variables = field_get_items('node', $node, 'field_variables')) {
      if (!array_filter($variables, 'deims_data_explorer_filter_allowed_column_variable')) {
        $data_explorer_problems[] = t('No variables selected to be available in the data explorer.');
      }
    }
    else {
      $data_explorer_problems[] = t('No variables saved in the data source.');
    }
  }

  if (!empty($data_explorer_problems)) {
    return array('<strong>' . t('DEIMS Data Explorer') . '</strong>: ' . t('Not available on the data source due to the following reasons:') . theme('item_list', array('items' => $data_explorer_problems)));
  }
}

/**
 * Get a database query object for the data explorer for a given data source.
 *
 * @param object $node
 *   A data source node object.
 *
 * @return SelectQuery|false
 *   A query object for the database specified in the DEIMS_DATA_EXPLORER_FIELD
 *   field on the data source.
 */
function deims_data_explorer_get_query_from_data_source($node, array $columns = array(), array $filters = array()) {
  if ($node->type == 'data_source' && $connection = field_get_items('node', $node, DEIMS_DATA_EXPLORER_FIELD)) {
    if (!empty($connection[0]['connection']) && !empty($connection[0]['table'])) {
      try {
        // Database::getConnection() could throw an exception so catch it and
        // fail gracefully.
        if ($dbo = Database::getConnection('default', $connection[0]['connection'])) {
          if ($dbo->schema()->tableExists($connection[0]['table'])) {
            $query = $dbo->select($connection[0]['table'], 'data');
            if (!empty($columns)) {
              $query->fields('data', $columns);
            }
            if (!empty($filters)) {
              deims_data_explorer_filters_apply($query, $filters);
            }
            return $query;
          }
        }
      }
      catch (Exception $e) {
        watchdog_exception('deims_data_explorer', $e);
        // Do not rethrow the exception. Fail gracefully by returning nothing.
      }
    }
  }

  return FALSE;
}

/**
 * Array filter callback; Checks if a variable field value can be a column.
 *
 * @param array $variable
 *   Contains settings.
 */
function deims_data_explorer_filter_allowed_column_variable($variable) {
  return !empty($variable['data']['data_explorer_settings']['expose']);
}

function deims_data_explorer_filters_apply(QueryAlterableInterface $query, array $filters) {
  foreach ($filters as $key => $filter) {
    switch ($filter['variable']['type']) {
      case '';
        if (drupal_strlen($filter['value'])) {
          $query->condition($key, '%' . $filter['value'] . '%', 'LIKE');
        }
        break;

      case 'physical':
      case 'date':
        if (drupal_strlen($filter['min'])) {
          $query->condition($key, $filter['min'], '>=');
        }
        if (drupal_strlen($filter['max'])) {
          $query->condition($key, $filter['max'], '<=');
        }
        break;

      case 'codes':
        if (!empty($filter['values'])) {
          $query->condition($key, $filter['values'], 'IN');
        }
        break;
    }
  }
}