diff --git a/includes/Jobs/GeneSearchIndexJob.php b/includes/Jobs/GeneSearchIndexJob.php index 26db99ac..a6f84a49 100644 --- a/includes/Jobs/GeneSearchIndexJob.php +++ b/includes/Jobs/GeneSearchIndexJob.php @@ -162,7 +162,7 @@ protected function loadBlastData($keys) { * @return array */ protected function loadAnnotations($keys) { - $query = "SELECT db.name AS db_name, dbxref.accession, cv.name AS cv_name, feature_id + $query = "SELECT db.name AS db_name, dbxref.accession, cv.name AS cv_name, feature_id FROM chado.dbxref INNER JOIN chado.cvterm ON dbxref.dbxref_id = cvterm.dbxref_id INNER JOIN chado.feature_cvterm ON cvterm.cvterm_id = feature_cvterm.cvterm_id diff --git a/includes/Jobs/TableIndexJob.php b/includes/Jobs/TableIndexJob.php index 46c36248..f58f76ba 100644 --- a/includes/Jobs/TableIndexJob.php +++ b/includes/Jobs/TableIndexJob.php @@ -52,7 +52,7 @@ public function __construct($index, $table, $fields) { $this->index = $index; $this->type = ucwords($index); $this->table = $table; - $this->fields = $fields; + $this->fields = array_map('db_escape_field', $fields); } /** @@ -69,17 +69,6 @@ public function handle() { elseif ($this->total > 0) { $es->createEntry($this->index, $this->table, FALSE, $records[0]); } - - $sql = " - SELECT F.uniquename, - F.feature_id, - BLAST.hit_description, - CVT.cvterm_id - FROM chado.feature F - FULL OUTER JOIN chado.blast_hit_data BLAST ON F.feature_id = BLAST.feature_id - FULL OUTER JOIN chado.feature_cvterm CVT ON F.feature_id = CVT.feature_id - WHERE BLAST.hit_description IS NOT NULL OR CVT.cvterm_id IS NOT NULL - "; } /** @@ -96,8 +85,8 @@ protected function get() { $this->offset(0); } - $select_fields = implode(',', $this->fields); - $query = "SELECT {$select_fields} FROM {{$this->table}} ORDER BY {$this->fields[0]} ASC OFFSET :offset LIMIT :limit"; + $select_fields = implode(', ', $this->fields); + $query = "SELECT {$select_fields} FROM {" . db_escape_table($this->table) . "} ORDER BY {$this->fields[0]} ASC OFFSET :offset LIMIT :limit"; return db_query($query, [ ':offset' => $this->offset, @@ -121,6 +110,6 @@ public function total() { * @return int */ public function count() { - return db_query('SELECT COUNT(*) FROM {' . $this->table . '}')->fetchField(); + return db_query('SELECT COUNT(*) FROM {' . db_escape_table($this->table) . '}')->fetchField(); } } \ No newline at end of file diff --git a/includes/TripalFields/local__feature_search/local__feature_search.inc b/includes/TripalFields/local__feature_search/local__feature_search.inc new file mode 100644 index 00000000..60ab6549 --- /dev/null +++ b/includes/TripalFields/local__feature_search/local__feature_search.inc @@ -0,0 +1,128 @@ + 'field_chado_storage', + // It is expected that all fields set a 'value' in the load() function. + // In many cases, the value may be an associative array of key/value pairs. + // In order for Tripal to provide context for all data, the keys should + // be a controlled vocabulary term (e.g. rdfs:type). Keys in the load() + // function that are supported by the query() function should be + // listed here. + 'searchable_keys' => array(), + ); + + // Provide a list of instance specific settings. These can be access within + // the instanceSettingsForm. When the instanceSettingsForm is submitted + // then Drupal with automatically change these settings for the instance. + // It is recommended to put settings at the instance level whenever possible. + // If you override this variable in a child class be sure to replicate the + // term_name, term_vocab, term_accession and term_fixed keys as these are + // required for all TripalFields. + public static $default_instance_settings = array( + // The DATABASE name, as it appears in chado.db. This also builds the link-out url. In most cases this will simply be the CV name. In some cases (EDAM) this will be the SUBONTOLOGY. + 'term_vocabulary' => 'local', + // The name of the term. + 'term_name' => 'feature_search', + // The unique ID (i.e. accession) of the term. + 'term_accession' => 'feature_search', + // 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, + // Indicates 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 have auto_attach set to FALSE so tha their values can be + // attached asynchronously. + 'auto_attach' => FALSE, + // The table in Chado that the instance maps to. + 'chado_table' => 'organism', + // The column of the table in Chado where the value of the field comes from. + 'chado_column' => 'organism_id', + // The base table. + 'base_table' => 'organism', + ); + + // A boolean specifying that users should not be allowed to create + // fields and instances of this field type through the UI. Such + // fields can only be created programmatically with field_create_field() + // and field_create_instance(). + public static $no_ui = FALSE; + + // A boolean specifying that the field will not contain any data. This + // should exclude the field from web services or downloads. An example + // could be a quick search field that appears on the page that redirects + // the user but otherwise provides no data. + public static $no_data = FALSE; + + /** + * Loads the field values from the underlying data store. + * + * @param $entity + * + * @return + * An array of the following format: + * $entity->{$field_name}['und'][0]['value'] = $value; + * where: + * - $entity is the entity object to which this field is attached. + * - $field_name is the name of this field + * - 'und' is the language code (in this case 'und' == undefined) + * - 0 is the cardinality. Increment by 1 when more than one item is + * available. + * - 'value' is the key indicating the value of this field. It should + * always be set. The value of the 'value' key will be the contents + * used for web services and for downloadable content. The value + * should be of the follow format types: 1) A single value (text, + * numeric, etc.) 2) An array of key value pair. 3) If multiple entries + * then cardinality should incremented and format types 1 and 2 should + * be used for each item. + * The array may contain as many other keys at the same level as 'value' + * but those keys are for internal field use and are not considered the + * value of the field. + * + * + */ + public function load($entity) { + + // ChadoFields automatically load the chado column specified in the + // default settings above. If that is all you need then you don't even + // need to implement this function. However, if you need to add any + // additional data to be used in the display, you should add it here. + parent::load($entity); + } +} \ No newline at end of file diff --git a/includes/TripalFields/local__feature_search/local__feature_search_formatter.inc b/includes/TripalFields/local__feature_search/local__feature_search_formatter.inc new file mode 100644 index 00000000..815a7eb6 --- /dev/null +++ b/includes/TripalFields/local__feature_search/local__feature_search_formatter.inc @@ -0,0 +1,131 @@ + 'default_value', + ]; + + /** + * Provides the field's setting form. + * + * This function corresponds to the hook_field_formatter_settings_form() + * function of the Drupal Field API. + * + * The settings form appears on the 'Manage Display' page of the content + * type administration page. This function provides the form that will + * appear on that page. + * + * To add a validate function, please create a static function in the + * implementing class, and indicate that this function should be used + * in the form array that is returned by this function. + * + * This form will not be displayed if the formatter_settings_summary() + * function does not return anything. + * + * param $field + * The field structure being configured. + * param $instance + * The instance structure being configured. + * param $view_mode + * The view mode being configured. + * param $form + * The (entire) configuration form array, which will usually have no use + * here. Typically for reference only. + * param $form_state + * The form state of the (entire) configuration form. + * + * @return + * A Drupal Form array containing the settings form for this field. + */ + public function settingsForm($view_mode, $form, &$form_state) { + + } + + /** + * Provides the display for a field + * + * This function corresponds to the hook_field_formatter_view() + * function of the Drupal Field API. + * + * This function provides the display for a field when it is viewed on + * the web page. The content returned by the formatter should only include + * what is present in the $items[$delta]['values] array. This way, the + * contents that are displayed on the page, via webservices and downloaded + * into a CSV file will always be identical. The view need not show all + * of the data in the 'values' array. + * + * @param $element + * @param $entity_type + * @param $entity + * @param $langcode + * @param $items + * @param $display + * + * @return + * An element array compatible with that returned by the + * hook_field_formatter_view() function. + */ + public function view(&$element, $entity_type, $entity, $langcode, $items, $display) { + // Get the settings + $settings = $display['settings']; + + $organism = $entity->chado_record; + $form = drupal_get_form('tripal_elasticsearch_gene_search_form', TRUE, "$organism->genus $organism->species", [ + 'ds_pane' => 'Feature Search', + ]); + + $content = '

You can search this organism’s features in the database by entering search terms in the box. You can search by feature name or annotation, for example, (Heat Shock, IPR020575, GO:0016049, etc.)

'; + $content .= drupal_render($form); + $element[] = [ + '#type' => 'markup', + '#markup' => $content, + ]; + } + + /** + * Provides a summary of the formatter settings. + * + * This function corresponds to the hook_field_formatter_settings_summary() + * function of the Drupal Field API. + * + * On the 'Manage Display' page of the content type administration page, + * fields are allowed to provide a settings form. This settings form can + * be used to allow the site admin to define how the field should be + * formatted. The settings are then available for the formatter() + * function of this class. This function provides a text-based description + * of the settings for the site developer to see. It appears on the manage + * display page inline with the field. A field must always return a + * value in this function if the settings form gear button is to appear. + * + * See the hook_field_formatter_settings_summary() function for more + * information. + * + * @param $field + * @param $instance + * @param $view_mode + * + * @return string + * A string that provides a very brief summary of the field settings + * to the user. + * + */ + public function settingsSummary($view_mode) { + return ''; + } + +} \ No newline at end of file diff --git a/includes/TripalFields/local__feature_search/local__feature_search_widget.inc b/includes/TripalFields/local__feature_search/local__feature_search_widget.inc new file mode 100644 index 00000000..115296bd --- /dev/null +++ b/includes/TripalFields/local__feature_search/local__feature_search_widget.inc @@ -0,0 +1,122 @@ + $value) { + if ($i > 0) { + $constructed_url .= '&'; + } + $constructed_url .= "{$key}={$value}"; + $i++; + + $form['hidden'][$key] = [ + '#type' => 'value', + '#value' => $value, + ]; + } + $form['#attributes']['id'] = 'cross-site-search-form'; $form['options'] = [ @@ -26,27 +43,35 @@ function tripal_elasticsearch_gene_search_form( ], ]; - $default_organism = ['' => 'Any Organism']; - $organism_list = chado_query('SELECT genus, species, common_name FROM {organism}')->fetchAll(); - $organisms = array_map(function ($organism) { - $name = "{$organism->genus} {$organism->species}"; - if (!empty($organism->common_name)) { - $name .= " ({$organism->common_name})"; - } - - return $name; - }, $organism_list); - - $form['options']['organism'] = [ - '#type' => 'select', - '#attributes' => [ - 'id' => 'tripal-elasticsearch-search-category', - 'style' => 'max-width: 250px;', - ], - '#options' => array_merge($default_organism, drupal_map_assoc($organisms)), - '#default_value' => isset($_GET['organism']) ? $_GET['organism'] : '', - '#required' => TRUE, - ]; + if ($organism !== NULL) { + $form['options']['organism'] = [ + '#type' => 'value', + '#value' => $organism, + ]; + } + else { + $default_organism = ['' => 'Any Organism']; + $organism_list = chado_query('SELECT genus, species, common_name FROM {organism}')->fetchAll(); + $organisms = array_map(function ($organism) { + $name = "{$organism->genus} {$organism->species}"; + if (!empty($organism->common_name)) { + $name .= " ({$organism->common_name})"; + } + + return $name; + }, $organism_list); + + $form['options']['organism'] = [ + '#type' => 'select', + '#attributes' => [ + 'id' => 'tripal-elasticsearch-search-category', + 'style' => 'max-width: 250px;', + ], + '#options' => array_merge($default_organism, drupal_map_assoc($organisms)), + '#default_value' => isset($_GET['organism']) ? $_GET['organism'] : '', + '#required' => TRUE, + ]; + } $form['options']['search_term'] = [ '#type' => 'textfield', @@ -55,7 +80,7 @@ function tripal_elasticsearch_gene_search_form( 'placeholder' => t('E,g. Kinase or IPR020405'), 'id' => 'tripal-elasticsearch-search-field', ], - '#description' => 'Examples: Heat Shock, IPR020575, GO:0016049, etc.', + '#description' => 'Examples: Heat Shock, IPR020575, GO:0016049, etc.', '#required' => TRUE, '#default_value' => isset($_GET['search_term']) ? $_GET['search_term'] : '', ]; @@ -89,7 +114,7 @@ function tripal_elasticsearch_gene_search_form( if (!empty($_GET['search_term']) || !empty($_GET['organism'])) { $form['results'] = [ '#type' => 'markup', - '#markup' => tripal_elasticsearch_gene_search_index_results(), + '#markup' => tripal_elasticsearch_gene_search_index_results(true), ]; } } @@ -125,9 +150,7 @@ function tripal_elasticsearch_gene_search_index_query_mapper($arguments) { } if (isset($arguments['search_term'])) { - $search_term = str_replace(':', ' AND ', $arguments['search_term']); - $queries[] = [ 'query_string' => [ 'query' => $search_term, @@ -192,7 +215,7 @@ function tripal_elasticsearch_gene_search_index_results_formatter( * * @return string */ -function tripal_elasticsearch_gene_search_index_results($formatted = TRUE) { +function tripal_elasticsearch_gene_search_index_results($display_download = false) { if (empty($_GET['search_term']) && empty($_GET['organism'])) { return ''; } @@ -220,7 +243,11 @@ function tripal_elasticsearch_gene_search_index_results($formatted = TRUE) { $content .= '' . $results['total'] . ' results found'; $content .= 'Page ' . $results['page'] . ' - ' . $results['pages'] . ''; $content .= ''; - $content .= 'Download results in CSV format'; + + if($display_download) { + $content .= 'Download results in CSV format'; + } + $content .= $formatted; $content .= $results['pager']; diff --git a/includes/indices_management.form.inc b/includes/indices_management.form.inc index b1c864c6..170c6741 100644 --- a/includes/indices_management.form.inc +++ b/includes/indices_management.form.inc @@ -206,12 +206,7 @@ function tripal_elasticsearch_indexing_form_validate($form, &$form_state) { $exposed = $form_state['values']['exposed']; $index_name = $form_state['values']['index_name']; - // TODO: check validity of URL instead of checking if it exists if ($type === "database") { - // URLs are required for database and gene_search. but only if they are exposed! - if (!$url && $exposed) { - form_set_error('url', t('Please provide a valid url')); - } // Index name validation. if (strlen($index_name) > 28) { form_set_error('index_name', t('String length cannot be greater than 28.')); @@ -227,10 +222,15 @@ function tripal_elasticsearch_indexing_form_validate($form, &$form_state) { use a different name.')); } // At least one table field need to be selected. - $table_fields = array_filter($form_state['values']['table_fields']); - if (empty($table_fields)) { + if (!is_array($form_state['values']['table_fields']) || empty($form_state['values']['table_fields'])) { form_set_error('table_fields', t('Please specify a mapping type for at least one field.')); } + else { + $table_fields = array_filter($form_state['values']['table_fields']); + if (empty($table_fields)) { + form_set_error('table_fields', t('Please specify a mapping type for at least one field.')); + } + } } elseif ($type === "gene_search_index") { if (!$url && $exposed) { @@ -454,7 +454,7 @@ function tripal_elasticsearch_index_edit_confirm( $table_name = $table_name[0]; } - db_query("INSERT INTO tripal_elasticsearch_indices (index_name, table_name, exposed, url) VALUES(:index_name, :table_name, 0, '')", [ + db_query("INSERT INTO {tripal_elasticsearch_indices} (index_name, table_name, exposed, url) VALUES(:index_name, :table_name, 0, '')", [ ':index_name' => $index_name, ':table_name' => $table_name, ]); @@ -725,9 +725,7 @@ function tripal_elasticsearch_index_delete_confirm_submit($form, &$form_state) { function tripal_elasticsearch_create_index(&$values) { $index_type = $values['index_type']; $exposed = $values['exposed'] ? 1 : 0; - if (isset($values['url'])) { - $url = $values['url']; - } + $url = isset($values['url']) ? $values['url'] : ''; // Populate settings for website or entities. if ($index_type === 'website') { @@ -827,6 +825,7 @@ function tripal_elasticsearch_create_index(&$values) { } // Dispatch jobs to populate the index + $job = NULL; switch ($index_type) { case 'website': $job = new NodesIndexJob(); @@ -835,6 +834,7 @@ function tripal_elasticsearch_create_index(&$values) { $job = new GeneSearchIndexJob($index_name); break; case 'entities': + EntitiesIndexJob::generateDispatcherJobs(); break; default: $fields = array_keys($field_mapping_types); @@ -843,10 +843,7 @@ function tripal_elasticsearch_create_index(&$values) { // All jobs other than entities can be dispatched directly. // Tripal entities must be dispatched by bundle type. - if ($index_type === 'entities') { - EntitiesIndexJob::generateDispatcherJobs(); - } - else { + if ($job !== NULL) { $dispatcher = new DispatcherJob($job); $dispatcher->dispatch(); } @@ -854,7 +851,10 @@ function tripal_elasticsearch_create_index(&$values) { $base_url = variable_get('website_base_url'); $cron_url = l($base_url . '/admin/config/system/cron', 'admin/config/system/cron'); - drupal_set_message("The indexing job for {$index_name} has been submitted to your CRON queue. You can view the status of your CRON jobs at {$cron_url}"); + drupal_set_message("The indexing job for {$index_name} has been submitted to your CRON queue. You can view the status of your CRON jobs at {$cron_url}"); + if ($index_type === 'database') { + drupal_set_message("You may create a search block for this index using the " . l('form management', 'admin/tripal/extension/tripal_elasticsearch/search_form_management') . " section."); + } drupal_goto('admin/tripal/extension/tripal_elasticsearch/indices_management'); } diff --git a/includes/search_form_management.form.inc b/includes/search_form_management.form.inc index 5b438d92..864ba272 100644 --- a/includes/search_form_management.form.inc +++ b/includes/search_form_management.form.inc @@ -349,7 +349,7 @@ function table_search_interface_building_form_submit($form, &$form_state) { } } - drupal_set_message("Search block set!"); + drupal_set_message("Search block set! You can enable the block from Admin > Structure > " . l('Blocks', 'admin/structure/block')); } /** diff --git a/includes/tripal_elasticsearch.fields.inc b/includes/tripal_elasticsearch.fields.inc new file mode 100644 index 00000000..4947ebdf --- /dev/null +++ b/includes/tripal_elasticsearch.fields.inc @@ -0,0 +1,125 @@ +data_table) AND ($bundle->data_table == 'organism')) { + // TODO: move to install + tripal_insert_cvterm(array( + 'id' => 'local:feature_search', + 'name' => 'feature_search', + 'cv_name' => 'local', + 'definition' => 'Search features related to an organism (search by property)', + )); + + // Then describe the field defined by that term. + $field_name = 'local__feature_search'; + $field_type = 'local__feature_search'; + $fields[$field_name] = array( + 'field_name' => $field_name, + 'type' => $field_type, + 'cardinality' => 1, + 'locked' => FALSE, + 'storage' => array( + 'type' => 'field_chado_storage', + ), + ); + } + + return $fields; +} + +/** + * Implements hook_bundle_instances_info(). + * + * This hook tells Drupal/Tripal to create a field instance of a given field type on a + * specific Tripal Content type (otherwise known as the bundle). Make sure to implement + * hook_bundle_create_fields() to create your field type before trying to create an + * instance of that field. + * + * @param $entity_type + * This should be 'TripalEntity' for all Tripal Content. + * @param $bundle + * This object describes the Type of Tripal Entity (e.g. Organism or Gene) the field + * instances are being created for. Thus this hook is called once per Tripal Content Type on your + * site. The name of the bundle is the machine name of the type (e.g. bio_data_1) and + * the label of the bundle (e.g. Organism) is what you see in the interface. Since the + * label can be changed by site admin, we suggest checking the data_table to determine + * if this is the entity you want to add field instances to. + * @return + * An array of field instance definitions. This is where you can define the defaults + * for any settings you use in your field. Each entry in this array will be used to + * create an instance of an already existing field. + */ +function tripal_elasticsearch_bundle_instances_info($entity_type, $bundle) { + $instances = array(); + + // ORGANISM. + //=============== + if (isset($bundle->data_table) AND ($bundle->data_table == 'organism')) { + $field_name = 'local__feature_search'; + $field_type = 'local__feature_search'; + $instances[$field_name] = array( + 'field_name' => $field_name, + 'entity_type' => $entity_type, + 'bundle' => $bundle->name, + 'label' => 'Feature Search', + 'description' => 'Search features related to an organism (search by property)', + 'required' => FALSE, + 'settings' => array( + 'auto_attach' => FALSE, + 'chado_table' => $bundle->data_table, + 'chado_column' => 'organism_id', + 'base_table' => $bundle->data_table, + ), + 'widget' => array( + 'type' => 'local__feature_search_widget', + 'settings' => array(), + ), + 'display' => array( + 'default' => array( + 'label' => 'hidden', + 'type' => 'local__feature_search_formatter', + 'settings' => array(), + ), + ), + ); + + } + + return $instances; +} diff --git a/tripal_elasticsearch.module b/tripal_elasticsearch.module index ee5ffc1c..3987fac4 100644 --- a/tripal_elasticsearch.module +++ b/tripal_elasticsearch.module @@ -12,6 +12,7 @@ require 'includes/indices_management.form.inc'; require 'includes/gene_search.form.inc'; require 'tripal_elasticsearch.ws.inc'; require 'includes/tuning.form.inc'; +require 'includes/tripal_elasticsearch.fields.inc'; // Auto discover and include jobs and ES classes. tripal_elasticsearch_auto_discover_classes(); @@ -434,23 +435,30 @@ function tripal_elasticsearch_generate_block($delta) { $block['subject'] = t('Search block form for index: ' . $index_name . ''); $page['form'] = drupal_get_form('tripal_elasticsearch_build_search_block_form', $index_name); + if (isset($_GET['op'])) { drupal_add_js(drupal_get_path('module', 'tripal_elasticsearch') . '/js/table_search_results_datatable.js'); $search_results = tripal_elasticsearch_paginate(10); - $markup = get_table_search_result_table($search_results['results'], $index_name, $search_results['total']); - $page_number = $search_results['page']; - $total_pages = ceil($search_results['total'] / 10); - - $page['download'] = [ - '#markup' => '

' . 'Download all results in csv format' . '

', - ]; - $page['count'] = [ - '#markup' => "
" . "

Showing page {$page_number} out of {$total_pages} pages.

" . "

Found {$search_results['total']} results.

" . "
", - ]; - $page['results'] = [ - '#markup' => $markup, - ]; + if (empty($search_results)) { + $page['content'] = ['#markup' => '0 results found.']; + } + else { + $markup = get_table_search_result_table($search_results['results'], $index_name, $search_results['total']); + $page_number = $search_results['page']; + $total_pages = ceil($search_results['total'] / 10); + + $page['download'] = [ + '#markup' => '

' . 'Download all results in csv format' . '

', + ]; + $page['count'] = [ + '#markup' => "
" . "

Showing page {$page_number} out of {$total_pages} pages.

" . "

Found {$search_results['total']} results.

" . "
", + ]; + $page['results'] = [ + '#markup' => $markup, + ]; + } } + $block['content'] = $page; } @@ -642,7 +650,7 @@ function tripal_elasticsearch_node_update($node) { try { $es = new ESInstance(); $indices = $es->getIndices(); - if(!in_array('website', $indices)) { + if (!in_array('website', $indices)) { return; } } catch (Exception $exception) { @@ -695,7 +703,7 @@ function tripal_elasticsearch_entity_update($entity, $entity_type) { try { $es = new ESInstance(); $indices = $es->getIndices(); - if(!in_array('entities', $indices)) { + if (!in_array('entities', $indices)) { return; } } catch (Exception $exception) {