diff --git a/includes/TingSearchCqlDoctor.class.inc b/includes/TingSearchCqlDoctor.class.inc new file mode 100644 index 0000000..47e55d0 --- /dev/null +++ b/includes/TingSearchCqlDoctor.class.inc @@ -0,0 +1,282 @@ + portland and (film) + * henning mortensen (f. 1939) => henning and mortensen and "(f. 1939)" + * harry and (White night) = harry and "(White night)" + * + * + * Valid cql stays the same eg. + * dkcclterm.sf=v and dkcclterm.uu=nt and (term.type=bog) not term.literaryForm=fiktion + */ + +/** + * Class TingSearchCqlDoctor. + * + * An attempt to convert non cql search phrases to valid cql. + */ +class TingSearchCqlDoctor { + + /** + * @var string cql_string. + * Internael representation of the searchphtrase. + */ + private $cql_string; + /** + * @var array pattern. + * The pattern to replace in searchstring. + */ + private $pattern = array(); + /** + * @var array replace. + * The string to replace with. + */ + private $replace=array(); + /** + * @var int replace_key. + * Used to prepend key - @see $this->get_replace_key(). + */ + private static $replace_key = 10; + + /** + * Constructor,Escape reserved characters, remove multiple whitespaces + * and sets private member $cql_string with trimmed string. + * + * @param string $string + * The search phrase to cure. + */ + public function __construct($string) { + $this->cql_string = trim($string); + // Remove multiple whitespaces. + $this->cql_string = preg_replace('/\s+/', ' ', $this->cql_string); + } + + /** + * Method to convert a string to strict cql. + * + * Basically this method adds quotes when needed. + * + * @param string $string + * The search query. + * @return string + * Cql compatible string. + */ + public function string_to_cql() { + + // Handle qoutes. + $this->fix_qoutes(); + // Hendle parantheses. + $this->fix_paranthesis(); + // Handle reserved characters. + $this->escape_reserved_characters(); + // Format the string. + return $this->format_cql_string(); + } + + /** + * Format cql string. + * + * Use private members $pattern and $replace to replace not valid cql phrases with valid. + * + * @return string + * string in valid cql (hopefully) + * + */ + private function format_cql_string() { + // Last check. All parts of cql string must be valid. + $valid = TRUE; + $parts = preg_split($this->get_cql_operators_regexp(), $this->cql_string); + foreach ($parts as $part) { + if (!$this->string_is_cql($part)) { + $valid = FALSE; + break; + } + } + // Explode string by whitespace. + $expressions = explode(' ', $this->cql_string); + + // Replace keys with phrases, + if (!empty($this->pattern)) { + $expressions = preg_replace($this->pattern, $this->replace, $expressions); + } + + $done = FALSE; + do { + $expressions = $this->replace_inline($expressions, $done); + } + while(!$done); + + // Remove empty elements. + $empty_slots = array_keys($expressions,''); + foreach($empty_slots as $slot){ + unset($expressions[$slot]); + } + + if ($valid) { + // Implode by blank. + return implode(' ', $expressions); + } + + // String is not valid; implode by and. + return implode(' and ', $expressions); + } + + + /** + * Some replacements are nested in paranthesis and/or qoutes + * eg. (hund and "("hest")") which is perfectly legal. + * Cql doctor first handles the qoutes and then the + * paranthesis ; thus (hund and "("hest")") becomes encoded multiple times. + * This method runs through all parts to fix it + * + * @param array $expressions; + * The parts of the searchquery to be cured. + * @param $done; + * Flag indicating whether all parts has been handled. + * @return array; + * Decoded expressions. + */ + private function replace_inline($expressions, &$done) { + foreach ($expressions as $key_exp => $expression) { + foreach ($this->pattern as $key_pat => $regexp) { + if (preg_match($regexp, $expression)) { + $expressions[$key_exp] = preg_replace($regexp, $this->replace[$key_pat], $expression); + return $expressions; + } + } + } + $done = TRUE; + return $expressions; + } + + + /** + * Enqoute forward slashes and '-'. + * + * @return nothing + * Alters private member cql_string. + */ + private function escape_reserved_characters() { + $this->cql_string = str_replace('/', ' "/" ', $this->cql_string); + } + + /** + * Get a key for replacement in string. + * + * @return string + */ + private function get_replace_key() { + $key_prefix = 'zxcv'; + return $key_prefix . self::$replace_key++; + } + + /** + * Handle parantheses. + * + * Look lace parantheses in string. If any found and content is not + * strict cql; enqoute the lot. + * + * @return nothing. + * Alters private member cql_string. + * + */ + private function fix_paranthesis() { + //Grab content in paranthesis. + preg_match_all('$\(([^\(\)]*)\)$', $this->cql_string, $phrases); + + if (empty($phrases[1])) { + // No matching paranthesis. + return; + } + + foreach ($phrases[1] as $key => $phrase) { + if (!$this->string_is_cql($phrase)) { + $this->set_replace_pattern($phrases[0][$key], TRUE); + } + else{ + $this->set_replace_pattern($phrase); + } + } + } + + /** + * Handle qoutes. + * + * Look for qouted content. Qouted content is replaced in searchstring. + */ + private function fix_qoutes() { + // Greab qouted content. + preg_match_all('$"([^"]*)"$', $this->cql_string, $phrases); +; + if (!empty($phrases[0])) { + foreach ($phrases[0] as $phrase) { + $this->set_replace_pattern($phrase); + } + } + } + + /** + * Helper function to set a single replacement key and phrase. + * + * Adds given phrase to private member $replace. Retrieve a key for the replacement and replace given phrase in + * internal representation of search string with the key. + * + * @param string $phrase. + * The phrase to add to private member $replace. + * @param bool $qoute_me. + * If TRUE phrase is enqouted. + * @return nothing + * Alters private member cql_string + */ + private function set_replace_pattern($phrase, $qoute_me = FALSE) { + if ($qoute_me) { + $this->replace[] = '"' . $phrase . '"'; + } + else { + $this->replace[] = $phrase; + } + $replace_key = $this->get_replace_key(); + $this->pattern[] = '/' . $replace_key . '/'; + + $this->cql_string = str_replace($phrase, $replace_key, $this->cql_string); + } + + + /** + * Tests if a string is cql. + * + * If string contains a cql operator it is assumed that an attempt to write cql is done. + * + * @param string $string + * The search query + * @return bool|int + * Whether the string is valid cql(TRUE) or not(FALSE) + */ + private function string_is_cql($string) { + // Single word is valid (no whitespaces). + if (strpos(trim($string), ' ') === FALSE) { + return TRUE; + } + return preg_match($this->get_cql_operators_regexp(), $string); + } + + /** + * Get reqular expression to ideniify cql operators. + * + * @return string. + * Reqular expression to identify cql operators. + */ + private function get_cql_operators_regexp() { + return '@ and | any | all | adj | or | not |=|\(|\)@i'; + } +} diff --git a/includes/ting_search.admin.inc b/includes/ting_search.admin.inc new file mode 100644 index 0000000..de8aed8 --- /dev/null +++ b/includes/ting_search.admin.inc @@ -0,0 +1,79 @@ + 'fieldset', + '#title' => t('Ting search settings'), + '#tree' => FALSE, + ); + + // Set ting search results per page. Used on the ting search page to determine + // the initial amount of search results to display. + $form['ting_search']['ting_search_results_per_page'] = array( + '#type' => 'textfield', + '#title' => t('Default results per page'), + '#description' => t('Enter the number of results desplayed on the search page by default.'), + '#default_value' => variable_get('ting_search_results_per_page', 10), + ); + + // Set ting search number of facets. Used on the ting search page to determine + // how many facets to display in the facet browser. + $form['ting_search']['ting_search_number_of_facets'] = array( + '#type' => 'textfield', + '#title' => t('Number of facets'), + '#description' => t('Enter the number of facets the search engine should show.'), + '#default_value' => variable_get('ting_search_number_of_facets', 25), + ); + + $form['ting_search']['ting_search_register_serie_title'] = array( + '#type' => 'textfield', + '#title' => t('Serie title'), + '#description' => t('Specify the searchstring to be used for searching against serie titles. Use @serietitle as a placeholder for the serietitle'), + '#default_value' => variable_get('ting_search_register_serie_title', 'phrase.titleSeries="@serietitle"'), + ); + + // The default sort option. Used by ting search execute to set the default + // sort option. + $form['ting_search']['ting_search_default_sort'] = array( + '#type' => 'select', + '#title' => t('Default sorting'), + '#options' => ting_search_sort_options(), + '#default_value' => variable_get('ting_search_default_sort', ''), + '#description' => t('Set the default sorting for search results.'), + ); + + $form['ting_search']['ting_search_result_message'] = array( + '#type' => 'item', + '#attributes' => array('class' => array('description')), + '#title' => t('Display message'), + '#markup' => t($display_message), + '#description' => t('Message to display when search has more than x results. The token %s will be substituted by the number choosen.'), + ); + + if ( t($display_message) != $display_message ) { + $form['ting_search']['ting_search_result_message']['#description'] .= '
' . t('Translated from: %n.', array('%n' => $display_message)); + } + + $form['ting_search']['ting_search_result_message_limit'] = array( + '#type' => 'textfield', + '#title' => t('Display message at'), + '#default_value' => variable_get('ting_search_result_message_limit', 100), + '#description' => t('The limit at wich the message is to be displayed.'), + ); + + return system_settings_form($form); +} diff --git a/js/ting_search.js b/js/ting_search.js new file mode 100644 index 0000000..dfe4268 --- /dev/null +++ b/js/ting_search.js @@ -0,0 +1,62 @@ +(function($) { + "use strict"; + + $(document).ready(function() { + // Add overlay with spinner to search input fields while searching. + $('input[name="search_block_form"]').keydown(function(event) { + // When enter is hit in the search form. + if (event.which == 13) { + Drupal.TingSearchOverlay(); + } + }); + + // Hook into the search button as well. + $('#search-block-form input[type="submit"]').click(function() { + Drupal.TingSearchOverlay(); + + return true; + }); + + // Add search link to the different links on the search result page. + $('.search-results a').live('click', function() { + if ($(this).not('[target="_blank"]').length) { + Drupal.TingSearchOverlay(); + } + }); + }); + + /** + * Add or remove spinner class to search block form wrapper. + * + * @param bool $state + * If TRUE the class spinner is added, FALSE it's removed. + */ + function ting_search_toggle_spinner(state) { + $('.search-field-wrapper').toggleClass('spinner', state); + } + + // Override default auto-complete to add spinner class. + Drupal.jsAC.prototype.setStatus = function (status) { + switch (status) { + case 'begin': + ting_search_toggle_spinner(true); + break; + case 'cancel': + case 'error': + case 'found': + ting_search_toggle_spinner(false); + break; + } + }; + + /** + * Override default auto-complete behavior that prevents form submit + */ + Drupal.autocompleteSubmit = function () { + $('#autocomplete').each(function () { + this.owner.hidePopup(); + }); + + return true; + }; +}(jQuery)); diff --git a/js/ting_search_backends.js b/js/ting_search_backends.js new file mode 100644 index 0000000..b251593 --- /dev/null +++ b/js/ting_search_backends.js @@ -0,0 +1,33 @@ +(function($) { + "use strict"; + + /** + * This script handle the changes from the different backends, which + * currently is the data-well and the homepage. This in sures that action + * is taken when the labels or radios are clicked. + */ + $(document).ready(function() { + // Click the label link when a radio button is clicked. + $('#ting-search-backend-engines-form input[type="radio"]').change(function() { + var link = $(this).parent().find('a'); + Drupal.TingSearchOverlay(); + window.location = link.attr('href'); + }); + + // When a label link is click, also check the radio button. + $('#ting-search-backend-engines-form a').click(function(event) { + var radio = $(this).parent().parent().find('input[type="radio"]'); + if (radio.is(':checked')) { + // If it is already checked do nothing. + event.preventDefault(); + return false; + } + else { + // Check the radio button and continue handling the click event. + radio.attr('checked', 'checked'); + Drupal.TingSearchOverlay(); + return true; + } + }); + }); +}(jQuery)); diff --git a/ting_search_extendform.js b/js/ting_search_extendform.js similarity index 99% rename from ting_search_extendform.js rename to js/ting_search_extendform.js index 6754da7..ef00349 100644 --- a/ting_search_extendform.js +++ b/js/ting_search_extendform.js @@ -8,7 +8,6 @@ $.TingExtendedForm = {}; $.TingExtendedForm.showExtended = false; - Drupal.behaviors.clearExtendForm = { attach:function(context, settings) { $('#extend-form-clear', context).click(function() { @@ -78,5 +77,5 @@ } }; -} (jQuery)); +}(jQuery)); diff --git a/js/ting_search_overlay.js b/js/ting_search_overlay.js new file mode 100644 index 0000000..cc12c37 --- /dev/null +++ b/js/ting_search_overlay.js @@ -0,0 +1,61 @@ +/** + * @file + * Defines the ting search overlay and makes it available for other scripts. + */ +(function($) { + "use strict"; + + var ctrlKeyIsPressed = false; + // Ctrl or CMD key codes. + var keyCodes = [224, 17, 91, 93]; + + // Do not show overlay if ctrl key is pressed. + $('body').live('keydown keyup', function(e) { + + var keyPressState = e.type == 'keydown' ? true : false; + if($.inArray(e.which, keyCodes) !== -1) { + ctrlKeyIsPressed = keyPressState; + } + }); + + /** + * Add search overlay function, so all search related JavaScripts can call it. + */ + Drupal.TingSearchOverlay = function(remove_overlay) { + + if (ctrlKeyIsPressed === false) { + // Try to get overlay + var overlay = $('.search-overlay--wrapper'); + if (!overlay.length) { + // Overlay not found so create is and display. + overlay = $('

' + Drupal.t('Searching please wait...') + '

' + Drupal.t('Cancel') + '

'); + $('body').prepend(overlay); + } + else { + // Overlay found. + if (typeof remove_overlay !== 'undefined') { + // If toggle remove it. + overlay.remove(); + } + } + } + }; + + // Hook into the overlays "Cancel" link and stop page loading if clicked. + $(document).ready(function() { + $('.search-overlay--wrapper .cancel').live('click', function() { + window.stop(); + Drupal.TingSearchOverlay(true); + }); + + // Remove overlay on page unload, so it's not shown when back button is used + // in the browser. + $(window).unload(function() { + var overlay = $('.search-overlay--wrapper'); + if (overlay.length) { + Drupal.TingSearchOverlay(true); + } + }); + }); + +}(jQuery)); diff --git a/js/ting_search_per_page.js b/js/ting_search_per_page.js new file mode 100644 index 0000000..1c5b012 --- /dev/null +++ b/js/ting_search_per_page.js @@ -0,0 +1,18 @@ +(function($) { + "use strict"; + + $(document).ready(function() { + $('.pane-search-per-page select').change(function() { + // Display search overlay. + Drupal.TingSearchOverlay(); + + // Reloads the page when new size is selected in the drop-down. + $('#ting-search-per-page-form').trigger("submit"); + }); + + $('.pane-ting-search-sort-form select').change(function() { + // Display search overlay. + Drupal.TingSearchOverlay(); + }); + }); +}(jQuery)); diff --git a/plugins/content_types/search_backends.inc b/plugins/content_types/search_backends.inc new file mode 100644 index 0000000..f152457 --- /dev/null +++ b/plugins/content_types/search_backends.inc @@ -0,0 +1,117 @@ + t('Ting search - backend engines'), + 'description' => t("Display the search engines as facets."), + 'single' => TRUE, + 'content_types' => array('ting_search'), + 'render callback' => 'ting_search_backend_engines_content_type_render', + 'category' => t('Ting'), + 'render last' => TRUE, + 'required context' => new ctools_context_required(t('Keywords'), 'string'), +); + +/** + * Render the ting search results amount block. + */ +function ting_search_backend_engines_content_type_render($subtype, $conf, $panel_args, $context) { + $options = array(); + + // Find search backends. + $backends = search_get_info(); + $default = search_get_default_module_info(); + $default_value = $default['module']; + + // Search keys. + $keys = NULL; + if (!empty($context) && isset($context->data)) { + $keys = $context->data; + } + + // Create options for each backend. + foreach ($backends as $name => $backend) { + // Get conditions (facets etc.). + $conditions = NULL; + if (isset($backend['conditions_callback']) && function_exists($backend['conditions_callback'])) { + // Build an optional array of more search conditions. + $conditions = $backend['conditions_callback']($keys); + } + + // Create search path. + $path = 'search/' . $backend['path'] . '/' . $keys; + if (empty($conditions['sort'])) { + // Remove sort condition, if is the empty string. + unset($conditions['sort']); + } + + // Create the option as a link. + $txt = format_string('Search @backend', array('@backend' => $backend['title'])); + $title = l(t($txt), $path, array('query' => $conditions)); + + // Set default value. + if (arg(1) == $backend['module']) { + $default_value = $backend['module']; + } + + // Add default option to the start of the array. + if ($default['module'] == $backend['module']) { + array_unshift($options, $title); + } + else { + $options[$backend['module']] = $title; + } + } + + // Create a form with the radio buttons. + $form = drupal_get_form('ting_search_backend_engines_form', $options, $default_value); + + // Create the output. + $block = new stdClass(); + $block->content = $form; + + return $block; +} + +/** + * Implements hook_form(). + * + * Defines the select search engine backend with radio buttons. + */ +function ting_search_backend_engines_form($form, &$form_state, $options, $default) { + $form = array( + '#token' => FALSE, + '#attached' => array( + 'js' => array( + // Script to handle usability in the frontend (clicks). + drupal_get_path('module', 'ting_search') . '/js/ting_search_backends.js', + ), + ), + ); + + $form['backends'] = array( + '#type' => 'fieldset', + '#title' => t('Search in'), + ); + + $form['backends']['searches'] = array( + '#type' => 'radios', + '#required' => FALSE, + '#options' => $options, + '#default_value' => $default, + ); + + return $form; +} + +/** + * Enable admin settings page. + */ +function ting_search_backend_engines_content_type_edit_form($form, &$form_state) { + return $form; +} + diff --git a/plugins/content_types/search_per_page.inc b/plugins/content_types/search_per_page.inc new file mode 100644 index 0000000..38b869d --- /dev/null +++ b/plugins/content_types/search_per_page.inc @@ -0,0 +1,147 @@ + t('Ting search - results per page'), + 'description' => t("Display results per page selector."), + 'content_types' => array('ting_search'), + 'render callback' => 'ting_search_per_page_content_type_render', + 'category' => t('Ting'), + 'required context' => new ctools_context_required(t('Keywords'), 'string'), + 'defaults' => array( + 'select_type' => array('dropdown'), + ), + 'render last' => TRUE, +); + +/** + * Render the ting search results amount block. + */ +function ting_search_per_page_content_type_render($subtype, $conf, $panel_args, $context) { + $sizes = ting_search_records_per_page_controls_content(); + + $content = ''; + if (!empty($sizes) && isset($conf['select_type'])) { + switch ($conf['select_type']) { + case 'list': + $items = array(); + foreach ($sizes as $size) { + $items[] = array( + 'data' => l($size['text'], current_path(), array( + 'query' => $size['query'], + 'attributes' => array( + 'class' => ($size['selected'] == TRUE ? array('selected') : array('')), + ), + )), + ); + } + + $content = array( + '#theme' => 'item_list', + '#title' => t('Results on page:'), + '#attributes' => array( + 'class' => array('ting-search-records-per-page'), + ), + '#items' => $items, + ); + break; + + case 'dropdown': + $content = drupal_get_form('ting_search_per_page_form', $sizes); + break; + } + } + + // Create the output. + $block = new stdClass(); + $block->content = $content; + + return $block; +} + +/** + * Implements hook_form(). + * + * Creates the select "number of results per page" form. + */ +function ting_search_per_page_form($form, &$form_state, $sizes) { + $form = array( + '#attached' => array( + 'js' => array( + // Script to handle usability in the frontend (clicks). + drupal_get_path('module', 'ting_search') . '/js/ting_search_per_page.js', + ), + ), + ); + + $options = array(); + $default = array(); + foreach ($sizes as $size) { + $options[$size['text']] = t('Number of search result per page: !num', array('!num' => $size['text'])); + + if ($size['selected'] == TRUE) { + $default = array($size['text']); + } + } + + $form['size'] = array( + '#type' => 'select', + '#required' => FALSE, + '#options' => $options, + '#default_value' => $default, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Size'), + '#states' => array( + 'visible' => array(':input[name="op"]' => array('value' => '')), + ), + ); + + return $form; +} + +/** + * Process submitted data for records per page form. + */ +function ting_search_per_page_form_submit($form, &$form_state) { + $query = drupal_get_query_parameters(); + + if (isset($form_state['input']['size'])) { + $query = array('size' => $form_state['input']['size']) + $query; + } + + $form_state['redirect'] = array(current_path(), array('query' => $query)); +} + +/** + * Enable admin settings page. + */ +function ting_search_search_per_page_content_type_edit_form($form, &$form_state) { + $form['select_type'] = array( + '#type' => 'select', + '#title' => 'Output type', + '#options' => array( + 'list' => 'HTML list', + 'dropdown' => 'Dropdown', + ), + '#default_value' => $form_state['conf']['select_type'], + ); + + return $form; +} + +/** + * Submit handler for search form. + */ +function ting_search_search_per_page_content_type_edit_form_submit($form, &$form_state) { + foreach (array_keys($form_state['plugin']['defaults']) as $key) { + if (isset($form_state['values'][$key])) { + $form_state['conf'][$key] = $form_state['values'][$key]; + } + } +} diff --git a/plugins/content_types/search_result.inc b/plugins/content_types/search_result.inc index c602df7..ed723e2 100644 --- a/plugins/content_types/search_result.inc +++ b/plugins/content_types/search_result.inc @@ -1,32 +1,31 @@ TRUE, - 'title' => t('Ting search - Search results'), - #'icon' => 'icon_search.png', - 'description' => t('The results of a search using keywords.'), - 'required context' => new ctools_context_required(t('Keywords'), 'string'), - 'category' => t('Widgets'), - 'content_types' => array('search_result'), - 'defaults' => array( - 'type' => 'node', - 'log' => TRUE, - 'override_empty' => FALSE, - 'empty_title' => '', - 'empty' => '', - 'empty_format' => filter_fallback_format(), - 'override_no_key' => FALSE, - 'no_key_title' => '', - 'no_key' => '', - 'no_key_format' => filter_fallback_format(), - ), - ); -} +$plugin = array( + 'single' => TRUE, + 'title' => t('Ting search - Search results'), + 'description' => t('The results of a search using keywords.'), + 'required context' => new ctools_context_required(t('Keywords'), 'string'), + 'category' => t('Widgets'), + 'content_types' => array('search_result'), + 'defaults' => array( + 'type' => 'node', + 'log' => TRUE, + 'override_empty' => FALSE, + 'empty_title' => '', + 'empty' => '', + 'empty_format' => filter_fallback_format(), + 'override_no_key' => FALSE, + 'no_key_title' => '', + 'no_key' => '', + 'no_key_format' => filter_fallback_format(), + ), + 'render first' => TRUE, + 'render last' => FALSE, +); /** * Render the custom content type. @@ -43,7 +42,7 @@ function ting_search_search_result_content_type_render($subtype, $conf, $panel_a $keys = $context->data; } - $conditions = NULL; + $conditions = NULL; if (isset($info['conditions_callback']) && function_exists($info['conditions_callback'])) { // Build an optional array of more search conditions. $conditions = $info['conditions_callback']($keys); @@ -52,6 +51,7 @@ function ting_search_search_result_content_type_render($subtype, $conf, $panel_a // Display nothing at all if no keywords were entered. if (empty($keys) && empty($conditions)) { if (!empty($conf['override_no_key'])) { + $block = new stdClass(); $block->title = $conf['no_key_title']; $block->content = check_markup($conf['no_key'], $conf['no_key_format'], FALSE); return $block; @@ -61,13 +61,12 @@ function ting_search_search_result_content_type_render($subtype, $conf, $panel_a // Build the content type block. $block = new stdClass(); - $block->module = 'search'; - $block->delta = 'result'; + $block->module = 'search'; + $block->delta = 'result'; $results = ''; // Only search if there are keywords or non-empty conditions. if ($keys || !empty($conditions)) { - // Collect the search results. $results = search_data($keys, $info['module'], $conditions); } @@ -78,15 +77,32 @@ function ting_search_search_result_content_type_render($subtype, $conf, $panel_a } if (!empty($results['#results'])) { - $output = "
    \n"; - foreach ($results['#results'] as $result) { - $output .= theme('search_result', array('result' => $result, 'module' => $conf['type'])); + // Get message limit and raw data well search results. + $limit = variable_get('ting_search_result_message_limit', 100); + $search_result = drupal_static('ting_search_results'); + + // Set message. + $message = ''; + + $frequency_rank = array( + 'rank_main_title' => t('Title'), + 'rank_creator' => t('Creator'), + 'rank_subject' => t('Subject') + ); + if (isset($search_result) && in_array($search_result->sortUsed, array_keys($frequency_rank))) { + $msg = t('Records are sorted by %sortUsed . Select another sort if it is not suited', array('%sortUsed' => $frequency_rank[$search_result->sortUsed])); + $message .= '
    ' . $msg . '
    '; + } + + + if (isset($search_result) && $limit < $search_result->numTotalObjects) { + $msg = t('Your search gave more than %s results. Try to search more specific or use the facets to filter the result.'); + $msg = sprintf($msg, $limit); + $message .= '
    ' . $msg . '
    '; } - $output .= "
\n"; - $output .= theme('ting_search_pager', array('tags' => NULL,)); $block->title = t('Search results'); - $block->content = $output; + $block->content = $message . theme('ting_search_results', array('results' => $results['#results'], 'module' => $conf['type']));; } else { if (empty($conf['override_empty'])) { @@ -153,7 +169,6 @@ function ting_search_search_result_content_type_edit_form($form, &$form_state) { '#title' => t('Display text if no search keywords were submitted'), ); - $form['no_key_title'] = array( '#title' => t('Title'), '#type' => 'textfield', diff --git a/plugins/content_types/search_result_count.inc b/plugins/content_types/search_result_count.inc new file mode 100644 index 0000000..78c628c --- /dev/null +++ b/plugins/content_types/search_result_count.inc @@ -0,0 +1,45 @@ + t('Ting search - page title'), + 'description' => t("Display title with result count."), + 'content_types' => array('ting_search'), + 'render callback' => 'ting_search_result_count_content_type_render', + 'category' => t('Ting'), + 'required context' => new ctools_context_required(t('Keywords'), 'string'), + 'render last' => TRUE, +); + +/** + * Render the ting search results amount block. + */ +function ting_search_result_count_content_type_render($subtype, $conf, $panel_args, $context) { + $block = new stdClass(); + + $search_result = drupal_static('ting_search_results'); + if (isset($search_result)) { + $results = isset($search_result->numTotalObjects) ? (int) $search_result->numTotalObjects : 0; + $string = format_plural($results > 1 ? $results : 1, 'Result', 'Results'); + $block->content = array( + '#prefix' => '

', + '#suffix' => '

', + '#markup' => t('Search result (!count !string)', array( + '!count' => $results, + '!string' => $string, + )), + ); + } + + return $block; +} + +/** + * Enable admin settings page. + */ +function ting_search_search_result_count_content_type_edit_form($form, &$form_state) { + return $form; +} diff --git a/plugins/content_types/ting_search.inc b/plugins/content_types/ting_search.inc index e35f61d..99912d6 100644 --- a/plugins/content_types/ting_search.inc +++ b/plugins/content_types/ting_search.inc @@ -2,7 +2,7 @@ $plugin = array( 'title' => t('Ting search - search results text'), - 'description' => t('Show a string with \'Showing x - y of z results'), + 'description' => t("Show a string with 'Showing x - y of z results'"), 'single' => TRUE, 'content_types' => array('ting_search'), 'render callback' => 'ting_search_content_type_render', @@ -19,22 +19,23 @@ function ting_search_content_type_render($subtype, $conf, $panel_args, $context) if ($pager_total[0] == TRUE) { $results = drupal_static('ting_search_results'); - $from = ($pager_page_array[0] < 1 ? 1 : ($pager_page_array[0])*$results->numTotalCollections+1); - $to = $from + ($results->numTotalCollections-1); + $from = ($pager_page_array[0] < 1 ? 1 : ($pager_page_array[0]) * $results->numTotalCollections + 1); + $to = $from + ($results->numTotalCollections - 1); $total = $results->numTotalObjects; $block->title = t('Search results text'); $block->content = '
' . format_plural($total, 'Show %from-%to of 1 result', - 'Show %from-%to of @count results', array( + 'Show %from-%to of @count results', array( '%from' => $from, '%to' => $to, - )) . '
'; + ) + ) . ''; } return $block; } /** - * Enable admin settings page + * Enable admin settings page. */ function ting_search_ting_search_content_type_edit_form($form, &$form_state) { return $form; diff --git a/templates/ting-search-display-extended-query.tpl.php b/templates/ting-search-display-extended-query.tpl.php new file mode 100644 index 0000000..88d6f48 --- /dev/null +++ b/templates/ting-search-display-extended-query.tpl.php @@ -0,0 +1,10 @@ + +

+ + +

diff --git a/ting-search-extended-search.tpl.php b/templates/ting-search-extended-search.tpl.php similarity index 63% rename from ting-search-extended-search.tpl.php rename to templates/ting-search-extended-search.tpl.php index 2ef3a3d..e370a6f 100644 --- a/ting-search-extended-search.tpl.php +++ b/templates/ting-search-extended-search.tpl.php @@ -1,6 +1,4 @@ -
- +
diff --git a/ting-search-results.tpl.php b/templates/ting-search-results.tpl.php similarity index 90% rename from ting-search-results.tpl.php rename to templates/ting-search-results.tpl.php index 780d587..c9d9b2f 100644 --- a/ting-search-results.tpl.php +++ b/templates/ting-search-results.tpl.php @@ -1,6 +1,4 @@ + '(term.acquisitionDate=201528* OR dkcclterm.kk=bkm201528* + OR term.acquisitionDate=201529* OR dkcclterm.kk=bkm201529* + OR term.acquisitionDate=20153* OR dkcclterm.kk=bkm20153*) + AND facet.date=2015 AND dkcclterm.dk=77.7 AND dkcclterm.ma=th', + + '(term.acquisitionDate=20153* OR dkcclterm.kk=bkm20153*) + AND facet.date=2015 AND dkcclterm.dk=sk AND (dkcclterm.ma=ro + OR dkcclterm.ma=no) AND dkcclterm.ma=xx NOT (dkcclterm.em=krimi + OR dkcclterm.ma=ss OR facet.category=børnematerialer)' + => + '(term.acquisitionDate=20153* OR dkcclterm.kk=bkm20153*) + AND facet.date=2015 AND dkcclterm.dk=sk AND (dkcclterm.ma=ro + OR dkcclterm.ma=no) AND dkcclterm.ma=xx NOT (dkcclterm.em=krimi + OR dkcclterm.ma=ss OR facet.category=børnematerialer)', + + '(term.acquisitionDate=20153* OR dkcclterm.kk=bkm20153*) + AND facet.date=2015 AND dkcclterm.ma=xx NOT (dkcclterm.em=krimi + OR dkcclterm.ma=ss OR facet.category=børnematerialer + OR dkcclterm.dk=sk OR dkcclterm.ma=ro OR dkcclterm.ma=no OR facet.dk5=79.41)' + => + '(term.acquisitionDate=20153* OR dkcclterm.kk=bkm20153*) + AND facet.date=2015 AND dkcclterm.ma=xx NOT (dkcclterm.em=krimi + OR dkcclterm.ma=ss OR facet.category=børnematerialer + OR dkcclterm.dk=sk OR dkcclterm.ma=ro OR dkcclterm.ma=no OR facet.dk5=79.41)', + + '(term.subject=fantasy OR term.subject="science fiction") + and (facet.type=dvd OR facet.type=blu*)' + => + '(term.subject=fantasy OR term.subject="science fiction") + and (facet.type=dvd OR facet.type=blu*)', + + 'facet.category="voksenmaterialer" AND term.type="bog" + AND term.subject=("gys" OR "overnaturlige evner")' + => + 'facet.category="voksenmaterialer" AND term.type="bog" + AND term.subject=("gys" OR "overnaturlige evner")', + + 'term.type="bog" AND facet.category="voksenmaterialer" + NOT dkcclterm.dk=(82* OR 83* OR 84* OR 85* OR 86* OR 87* OR 88*)' + => + 'term.type="bog" AND facet.category="voksenmaterialer" + NOT dkcclterm.dk=(82* OR 83* OR 84* OR 85* OR 86* OR 87* OR 88*)', + + 'rec.id = 870970-basis:05203120 OR rec.id=870970-basis:05203120' + => + 'rec.id = 870970-basis:05203120 OR rec.id=870970-basis:05203120', + + 'em any "delebørn skilsmissebørn"' => 'em any "delebørn skilsmissebørn"', + + 'blue-ray' => 'blue-ray', + ); +} + +function ting_search_test_invalid_cql() { + return array( + 'anders and' => 'anders and and', + + 'anders AND' => 'anders and AND', + + '"anders and" phrase.title=ander*' + => + '"anders and" and phrase.title=ander*', + + 'anders AND (dc.title=historie)' + => + 'anders AND (dc.title=historie)', + + 'hest fisk hund' => 'hest and fisk and hund', + 'hest fisk and hund' => 'hest and fisk and and and hund', + 'blue/ray' => 'blue and "/" and ray', + ); +} \ No newline at end of file diff --git a/ting_search.test b/tests/ting_search.test similarity index 81% rename from ting_search.test rename to tests/ting_search.test index 7cd7512..d8b419b 100644 --- a/ting_search.test +++ b/tests/ting_search.test @@ -16,8 +16,6 @@ class TingSearchTestCase extends DrupalWebTestCase { parent::setUp('ting', 'ding_entity', 'search', 'ting_search', 'nanosoap'); variable_set('ting_agency', '100200'); variable_set('ting_search_url', 'http://opensearch.addi.dk/next_2.0/'); - variable_set('ting_scan_url', 'http://openscan.addi.dk/1.5/'); - variable_set('ting_spell_url', 'http://openspell.addi.dk/1.2/'); variable_set('ting_recommendation_url', 'http://openadhl.addi.dk/1.1/'); variable_set('search_active_modules', array('ting_search' => 'ting_search')); variable_set('ting_search_profile', 'opac'); @@ -174,6 +172,35 @@ class TingSearchParsingTestCase extends DrupalUnitTestCase { 'dc.title' => 'flammernes pokal', ), ), + // Regression tests. + array( + 'string' => 'banana and (dk=sk)', + 'keys' => array('term.creator', 'term.title', 'term.subject'), + 'expected' => array( + 'q' => 'banana and (dk=sk)', + ), + ), + array( + 'string' => 'banana AND (dk=sk)', + 'keys' => array('term.creator', 'term.title', 'term.subject'), + 'expected' => array( + 'q' => 'banana and (dk=sk)', + ), + ), + array( + 'string' => '(dk=sk)', + 'keys' => array('term.creator', 'term.title', 'term.subject'), + 'expected' => array( + 'q' => '(dk=sk)', + ), + ), + array( + 'string' => '(term.type="bog" OR term.type="ebog") AND (term.subject="fantasy" OR term.subject="krimi")', + 'keys' => array('term.creator', 'term.title', 'term.subject'), + 'expected' => array( + 'q' => '(term.type="bog" OR term.type="ebog") and (term.subject="fantasy" OR term.subject="krimi")', + ), + ), ); foreach ($testCases as $test) { drupal_static_reset('ting_search_extract_indexes'); @@ -187,45 +214,30 @@ class TingSearchParsingTestCase extends DrupalUnitTestCase { function testQuoting() { drupal_load('module', 'ting_search'); - $testCases = array( - array( - 'string' => 'anders and', - 'expected' => 'anders "and"', - ), - array( - 'string' => 'anders aNd', - 'expected' => 'anders "aNd"', - ), - array( - // This might seem like a pretty simple case, but this is actually a - // regression test for an issue not caught by the more advanced tests. - 'string' => 'anders AND', - 'expected' => 'anders AND', - ), - array( - 'string' => 'anders and AND (dc.title=historie)', - 'expected' => 'anders "and" AND (dc.title=historie)', - ), - array( - 'string' => 'anders AND (dc.title=historie)', - 'expected' => 'anders AND (dc.title=historie)', - ), - array( - 'string' => '"anders and" (dc.title=historie)', - 'expected' => '"anders and" (dc.title=historie)', - ), - array( - 'string' => '"anders \"and\"" (dc.title=historie)', - 'expected' => '"anders \"and\"" (dc.title=historie)', - ), + require_once "strings_for_cql_test.php"; - ); - foreach ($testCases as $test) { - $res = _ting_search_quote($test['string']); - $this->assertEqual($res, $test['expected'], 'Properly quoted: ' . $test['string']); - if ($res != $test['expected']) { + $testCases = ting_search_test_valid_cql(); + + foreach ($testCases as $search => $expected) { + $res = _ting_search_quote($search); + $string = preg_replace( "/\r|\n/", "", $search ); + $result = preg_replace( "/\r|\n/", "", $expected ); + $this->assertEqual($string,$result , 'Properly quoted: ' . $search); + if ($string != $result) { + debug($res); + } + } + + $testCases = ting_search_test_invalid_cql(); + + foreach ($testCases as $search => $expected) { + $res = _ting_search_quote($search); + $this->assertEqual($res,$expected , 'Properly quoted: ' . $search); + if ($res != $expected) { debug($res); + debug($search); } } + } } diff --git a/ting-search-display-extended-query.tpl.php b/ting-search-display-extended-query.tpl.php deleted file mode 100644 index a18a15f..0000000 --- a/ting-search-display-extended-query.tpl.php +++ /dev/null @@ -1,12 +0,0 @@ - -

- - -

diff --git a/ting_search.info b/ting_search.info index c1b7f97..5933e14 100644 --- a/ting_search.info +++ b/ting_search.info @@ -4,7 +4,6 @@ package = Ding! version = "7.x-0.26" core = 7.x files[] = ting_search.module -files[] = ting_search.test +files[] = tests/ting_search.test dependencies[] = ting dependencies[] = search -stylesheets[all][] = ting_search.css diff --git a/ting_search.make b/ting_search.make index c0e49fb..b904ae6 100644 --- a/ting_search.make +++ b/ting_search.make @@ -5,4 +5,4 @@ core = 7.x projects[ting][type] = "module" projects[ting][download][type] = "git" projects[ting][download][url] = "git@github.com:ding2/ting.git" -projects[ting][download][tag] = "7.x-0.22" +projects[ting][download][branch] = "master" diff --git a/ting_search.module b/ting_search.module index 6cef62d..196e235 100644 --- a/ting_search.module +++ b/ting_search.module @@ -1,10 +1,21 @@ 1); } } + +/** + * Implements hook_menu(). + */ +function ting_search_menu() { + $items = array(); + + $items['admin/config/ting/search'] = array( + 'title' => 'Search settings', + 'description' => 'Settings avialable for ting search.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ting_search_admin_settings'), + 'access arguments' => array('administer ting settings'), + 'file' => 'includes/ting_search.admin.inc', + ); + + $items['search-blank'] = array( + 'callback' => '_blank_page', + 'access' => TRUE, + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Menu callback to fix the twice search issue. + * + * When the search form is submitted back to Drupal it's submitted back to the + * same page. So a search performed on the search page will trigger the first + * search and the new search (twice search). + * + * @see http://platform.dandigbib.org/issues/372 + */ +function _blank_page() { + return ''; +} + /** * Implements hook_menu_alter(). * * Temporary hack to alter titles. */ function ting_search_menu_alter(&$items) { - $items['search/node']['title'] = 'Hjemmeside'; - $items['search/node/%menu_tail']['title'] = 'Hjemmeside'; + $items['search/node']['title'] = 'Homepage'; + $items['search/node/%menu_tail']['title'] = 'Homepage'; $items['search/node/%menu_tail']['load arguments'] = array('%map', '%index'); - $items['search/ting']['title'] = 'Brønd'; - $items['search/ting/%menu_tail']['title'] = 'Brønd'; + $items['search/ting']['title'] = 'Data well'; + $items['search/ting/%menu_tail']['title'] = 'Data well'; $items['search/meta/%menu_tail']['load arguments'] = array('%map', '%index'); $items['search/meta']['title'] = 'Universal Search'; $items['search/meta/%menu_tail']['title'] = 'Universal Search'; @@ -57,79 +106,96 @@ function ting_search_search_info() { * Implements hook_ding_facetbrowser(). */ function ting_search_ding_facetbrowser() { - $results = new stdClass(); + $results = new stdClass(); $results->show_empty = FALSE; - $search_result = drupal_static('ting_search_results'); + $search_result = drupal_static('ting_search_results'); if ($search_result) { - $results->facets = ($search_result instanceof TingClientSearchResult) ? $search_result->facets : array(); - $results->searchkey = $search_result->search_key; + $results->facets = ($search_result instanceof TingClientSearchResult) ? $search_result->facets : array(); + $results->searchkey = $search_result->search_key; return $results; } } -/** - * Implements hook_entity_info_alter(). - */ -function ting_search_entity_info_alter(&$entity_info) { - $entity_info['ting_collection']['view modes'] += array( - 'search_result' => array( - 'label' => t('Search result'), - 'custom settings' => FALSE, - ), - ); -} - /** * Implements hook_theme(). */ -function ting_search_theme() { +function ting_search_theme($existing, $type, $theme, $path) { return array( 'ting_search_results' => array( 'variables' => array('results' => NULL, 'module' => NULL), 'file' => 'ting_search.pages.inc', - 'template' => 'ting-search-results', + 'template' => 'templates/ting-search-results', ), 'ting_search_mini_pager' => array( - 'variables' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9), + 'variables' => array( + 'tags' => array(), + 'element' => 0, + 'parameters' => array(), + 'quantity' => 9, + ), ), 'ting_search_pager' => array( - 'variables' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9), + 'variables' => array( + 'tags' => array(), + 'element' => 0, + 'parameters' => array(), + 'quantity' => 9, + ), ), 'ting_search_display_extended_query' => array( - 'variables' => array('query_label'=>NULL,'query_string' => NULL), + 'variables' => array('query_label' => NULL,'query_string' => NULL), 'template' => 'ting-search-display-extended-query', + 'path' => $path . '/templates', ), 'ting_search_extended_search' => array( 'template' => 'ting-search-extended-search', + 'path' => $path . '/templates', ), ); } +/** + * Returns a themed list of extended actions. + * + * @return string + * HTML ting_search_extended_search. + */ function ting_search_get_extended_actions() { return theme('ting_search_extended_search'); } /** - * @brief Implementation of hook_form_FORM_ID_alter() for form search_block_form. + * Implements hook_form_FORM_ID_alter(). + * + * For the form search_block_form. */ function ting_search_form_search_block_form_alter(&$form, &$form_state, $form_id) { + // Define advanced fields. $advanced_fields = array( - 'dc.creator' => array( + 'term.creator' => array( 'key' => 'creator', 'title' => t('Author'), 'description' => t('Enter the author name'), ), - 'dc.title' => array( + 'term.title' => array( 'key' => 'title', 'title' => t('Title'), 'description' => t('Enter title'), ), - 'dc.subject' => array( + 'term.subject' => array( 'key' => 'subject', 'title' => t('Subject'), 'description' => t('Enter subject keywords'), ), ); + + // Remove the form token so the form becomes cachable. If we don't, all + // pages with the search form becomes un-cachable, as drupal_validate_form() + // will check the token and fail if the current user is not the same as the + // one that caused the page to be cached. + unset($form['#token']); + unset($form['form_token']); + // We're going to disable advanced search in // the first version, and implement later on. // When implementing againg, set @@ -140,6 +206,7 @@ function ting_search_form_search_block_form_alter(&$form, &$form_state, $form_id // Parse extended search query parameters. if (arg(0) == 'search') { $parts = explode('/', $_GET['q']); + // Lose 'search' and the search type. array_shift($parts); $type = array_shift($parts); @@ -147,15 +214,27 @@ function ting_search_form_search_block_form_alter(&$form, &$form_state, $form_id $indexes = ting_search_extract_keys($search_query, array_keys($advanced_fields)); $search_query = $indexes['q']; unset($indexes['q']); + if ($type != 'ting' and !empty($indexes)) { $search_query .= " " . implode(' ', $indexes); $indexes = array(); $advanced = FALSE; } + // Set default value to the current search query. $form['search_block_form']['#default_value'] = $search_query; } + // Set max length on search input. This is need when users uses CQL search + // string as they can get very long. This length is base on experience from + // ding1 as seams to be long enough. + $form['search_block_form']['#maxlength'] = 1024; + + // Add JS to handle spinner in the search form. + $form['search_block_form']['#attached']['js'] = array( + drupal_get_path('module', 'ting_search') . '/js/ting_search.js', + ); + $form['sort'] = array( '#type' => 'hidden', '#default_value' => isset($_GET['sort']) ? check_plain($_GET['sort']) : FALSE, @@ -163,11 +242,11 @@ function ting_search_form_search_block_form_alter(&$form, &$form_state, $form_id ); $form['size'] = array( '#type' => 'hidden', - '#default_value' => isset($_GET['size']) ? (int)$_GET['size'] : FALSE, + '#default_value' => isset($_GET['size']) ? (int) $_GET['size'] : FALSE, '#attributes' => array('id' => 'controls_search_size'), ); - // See line 127-130 - disable in the first version + // See line 127-130 - disabled in the first version. //$form['form_id']['#suffix'] = ting_search_get_extended_actions(); if ($advanced) { @@ -180,39 +259,43 @@ function ting_search_form_search_block_form_alter(&$form, &$form_state, $form_id '#prefix' => '
', '#suffix' => '
', '#attached' => array( - 'css' => array( - drupal_get_path('module', 'ting_search') . '/ting_search_extendform.css', - ), 'js' => array( - drupal_get_path('module', 'ting_search') . '/ting_search_extendform.js', + drupal_get_path('module', 'ting_search') . '/js/ting_search_extendform.js', ), ), ); - $expand = FALSE; foreach ($advanced_fields as $name => $field) { $form['advanced'][$field['key']] = array( '#type' => 'textfield', - '#title' => $field['title'], + '#title' => check_plain($field['title']), '#size' => 30, '#maxlength' => 64, - '#description' => $field['description'], + '#description' => check_plain($field['description']), '#default_value' => isset($indexes[$name]) ? $indexes[$name] : '', ); } } $form['#submit'] = array('ting_search_submit'); + // This submits the form to a blank page and prevents the search to be + // executed twice. See http://platform.dandigbib.org/issues/372 for more + // information. + $form['#action'] = '?q=search-blank'; + return $form; } /** * Extract special field keys from search string. * - * @param string $search_query The search query. - * @param array $keys Keys to extract. + * @param string $search_query + * The search query. + * @param array $keys + * Keys to extract. * - * @return array Where the array keys are the search keys, and the remainder + * @return array + * Where the array keys are the search keys, and the remainder * search string in 'q'. */ function ting_search_extract_keys($search_query, $keys) { @@ -226,12 +309,14 @@ function ting_search_extract_keys($search_query, $keys) { $search_query = preg_replace_callback($regexp, 'ting_search_extract_indexes', $search_query); } + // Remove any leading and's. + $search_query = preg_replace('/^ and \\(/', '(', $search_query); $indexes['q'] = $search_query; return $indexes; } /** - * preg_replace_callback function. + * Preg_replace_callback function. */ function ting_search_extract_indexes($matches, $set_keys = NULL) { static $keys; @@ -242,15 +327,15 @@ function ting_search_extract_indexes($matches, $set_keys = NULL) { $subexps = preg_split('/\s+and\s+/i', $matches[2], NULL, PREG_SPLIT_NO_EMPTY); $indexes = &drupal_static(__FUNCTION__, array()); foreach ($subexps as $subexp) { - if ((preg_match('/^([^=]+)\=([^"]*)$/', $subexp, $rx) || preg_match('/^([^=]+)\="(.*)"$/', $subexp, $rx)) && array_key_exists(trim($rx[1]), $keys)) { + if ((preg_match('/^([^=]+)\=([^"]*)$/', $subexp, $rx) || preg_match('/^([^=]+)\="([^"]*)"$/', $subexp, $rx)) && array_key_exists(trim($rx[1]), $keys)) { $indexes[trim($rx[1])] = trim($rx[2]); } else { $return[] = $subexp; } } - // Reappend unknown stuff. - if (sizeof($return)) { + // Re-append unknown stuff. + if (count($return)) { return " and (" . implode(' and ', $return) . ")"; } return ""; @@ -267,14 +352,16 @@ function ting_search_conditions_callback($keys) { } if (!empty($_REQUEST['size'])) { - $conditions['size'] = (int)$_REQUEST['size']; + $conditions['size'] = (int) $_REQUEST['size']; } if (!empty($_REQUEST['sort'])) { $conditions['sort'] = check_plain($_REQUEST['sort']); } - + else { + $conditions['sort'] = variable_get('ting_search_default_sort', ''); + } // If facets is set, check if we have to remove any, if so, // reload the page. if (!empty($_REQUEST['facets'])) { @@ -300,7 +387,7 @@ function ting_search_conditions_callback($keys) { if ($redirect === TRUE) { $facets = array(); foreach ($conditions['facets'] as $facet) { - $facets['facets'][] = $facet; + $facets['facets'][] = $facet; } drupal_goto(rawurldecode($_GET['q']), array('query' => $facets)); } @@ -312,67 +399,91 @@ function ting_search_conditions_callback($keys) { * Implements hook_search_execute(). */ function ting_search_search_execute($keys = NULL, $conditions = NULL) { - // TODO: Set sort options - $options = array(); - $results = array(); - $facetArray = array(); - $query = '(' . _ting_search_quote($keys) . ')'; - $options['numFacets'] = 25; - module_load_include('client.inc', 'ting'); - //Extend query with selected facets - if (isset($conditions['facets']) && $conditions['facets'] != NULL) { - $facets = $conditions['facets']; - foreach ($facets as $facet) { - $facet = explode(':', $facet, 2); - if ($facet[0]) { - $facetArray[] = $facet[0] . '="' . rawurldecode($facet[1]) . '"'; + // Use static variable to ensure that this is only executed once. + $results = &drupal_static(__FUNCTION__); + if (!isset($results)) { + $results = array(); + $options = array(); + $results = array(); + $facet_array = array(); + $query = '(' . _ting_search_quote($keys) . ')'; + $options['numFacets'] = variable_get('ting_search_number_of_facets', 25); + + // Extend query with selected facets. + if (isset($conditions['facets']) && $conditions['facets'] != NULL) { + $facets = $conditions['facets']; + foreach ($facets as $facet) { + $facet = explode(':', $facet, 2); + if ($facet[0]) { + $facet_array[] = $facet[0] . '="' . rawurldecode($facet[1]) . '"'; + } } + + $query .= ' AND ' . implode(' AND ', $facet_array); } - $query .= ' AND ' . implode(' AND ', $facetArray); - } - try { - $page = pager_find_page(); + $search_result = NULL; + try { + $page = pager_find_page(); - $resultsPerPage = variable_get('ting_search_results_per_page', 10); - if (!empty($conditions['size'])) { - $resultsPerPage = $conditions['size']; - } + // Set results per page. + $results_per_page = variable_get('ting_search_results_per_page', 10); + if (!empty($conditions['size'])) { + $results_per_page = $conditions['size']; + } - if (!empty($conditions['sort'])) { - $options['sort'] = $conditions['sort']; + // Set sort order. + if (!empty($conditions['sort'])) { + $options['sort'] = $conditions['sort']; + } + + module_load_include('client.inc', 'ting'); + $search_result = ting_do_search($query, $page + 1, $results_per_page, $options); + if (isset($search_result->collections)) { + $search_result->search_key = $keys; + + // So we got the total amount of results, but the joker here is that + // we have no clue how many collections we got. + // Rounded up, this will display 'one more page' if $search-result->more + // is true. + $total_results = ($page + 1) * $results_per_page + ($search_result->more ? 1 : 0); + + pager_default_initialize($total_results, $results_per_page); + + foreach ($search_result->collections as &$collection) { + $build = ting_collection_view($collection, 'teaser'); + $uri = entity_uri('ting_collection', $collection); + $results[] = array( + 'link' => url($uri['path'], $uri['options']), + 'type' => '', + 'title' => $collection->title, + 'user' => '', + 'date' => '', + 'snippet' => drupal_render($build), + ); + } + } + } + catch (TingClientException $e) { + watchdog_exception('ting_search', $e); + $results = array(); } - $searchResult = ting_do_search($query, $page + 1, $resultsPerPage, $options); - if (isset($searchResult->collections)) { - $searchResult->search_key = $keys; - - // TODO: caching highes total_result know value of specific search - // at the moment we only know if there is one more page - $total_results = ($page + 1) * $resultsPerPage + ($searchResult->more ? 1 : 0); - - pager_default_initialize($total_results, $resultsPerPage); - - foreach ($searchResult->collections as &$collection) { - $build = ting_collection_view($collection, 'teaser'); - $uri = entity_uri('ting_collection', $collection); - $results[] = array( - 'link' => url($uri['path'], $uri['options']), - 'type' => '', - 'title' => $collection->title, - 'user' => '', - 'date' => '', - 'snippet' => drupal_render($build), - ); + // Save search result for usage by facet modules etc. + $ting_search_results = &drupal_static('ting_search_results'); + $ting_search_results = $search_result; + + // Add support for facet api. + if (module_exists('ding_facets')) { + // @todo ITK Aarhus finish implementation of facet api to use a standard + // facet module, that allows usage of modules from D.O. to improved facet + // usage. + $adapter = facetapi_adapter_load('ding_facets'); + if ($adapter) { + $adapter->addActiveFilters($query); } } } - catch (TingClientException $e) { - // TODO: Log the error. - $results = array(); - } - - drupal_static('ting_search_results', $searchResult); return $results; } @@ -389,7 +500,7 @@ function ting_search_search_page($results) { } /** - * Theme a pager + * Theme a pager. */ function theme_ting_search_pager($variables) { $tags = $variables['tags']; @@ -402,15 +513,14 @@ function theme_ting_search_pager($variables) { // Calculate various markers within this pager piece: // Middle is used to "center" pages around the current page. $pager_middle = ceil($quantity / 2); - // current is the page we are currently paged to + // Current is the page we are currently paged to. $pager_current = $pager_page_array[$element] + 1; - // first is the first page listed by this pager piece (re quantity) + // First is the first page listed by this pager piece (re quantity). $pager_first = $pager_current - $pager_middle + 1; - // last is the last page listed by this pager piece (re quantity) + // Last is the last page listed by this pager piece (re quantity). $pager_last = $pager_current + $quantity - $pager_middle; - // max is the maximum page number + // Max is the maximum page number. $pager_max = $pager_total[$element]; - // End of marker calculations. // Prepare for generation loop. $i = $pager_first; @@ -424,26 +534,43 @@ function theme_ting_search_pager($variables) { $pager_last = $pager_last + (1 - $i); $i = 1; } - // End of generation loop preparation. - $li_previous = theme('pager_previous', array('text' => (isset($tags[1]) ? $tags[1] : t('‹ previous')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters)); + $li_previous = theme('pager_previous', array( + 'text' => isset($tags[1]) ? $tags[1] : t('‹ previous'), + 'element' => $element, + 'interval' => 1, + 'parameters' => $parameters, + )); if (empty($li_previous)) { $li_previous = " "; } - $li_first = theme('pager_first', array('text' => (isset($tags[0]) ? $tags[0] : t('« first')), 'element' => $element, 'parameters' => $parameters)); + $li_first = theme('pager_first', array( + 'text' => isset($tags[0]) ? $tags[0] : t('« first'), + 'element' => $element, + 'parameters' => $parameters, + )); if (empty($li_first)) { $li_first = " "; } - $li_next = theme('pager_next', array('text' => (isset($tags[3]) ? $tags[3] : t('next ›')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters)); + $li_next = theme('pager_next', array( + 'text' => isset($tags[3]) ? $tags[3] : t('next ›'), + 'element' => $element, + 'interval' => 1, + 'parameters' => $parameters, + )); if (empty($li_next)) { $li_next = " "; } - $li_last = theme('pager_last', array('text' => (isset($tags[4]) ? $tags[4] : t('last »')), 'element' => $element, 'parameters' => $parameters)); + $li_last = theme('pager_last', array( + 'text' => isset($tags[4]) ? $tags[4] : t('last »'), + 'element' => $element, + 'parameters' => $parameters, + )); if (empty($li_last)) { $li_last = " "; @@ -475,7 +602,12 @@ function theme_ting_search_pager($variables) { if ($i < $pager_current) { $items[] = array( 'class' => array('pager-item'), - 'data' => theme('pager_previous', array('text' => $i, 'element' => $element, 'interval' => ($pager_current - $i), 'parameters' => $parameters)), + 'data' => theme('pager_previous', array( + 'text' => $i, + 'element' => $element, + 'interval' => ($pager_current - $i), + 'parameters' => $parameters, + )), ); } if ($i == $pager_current) { @@ -487,7 +619,12 @@ function theme_ting_search_pager($variables) { if ($i > $pager_current) { $items[] = array( 'class' => array('pager-item'), - 'data' => theme('pager_next', array('text' => $i, 'element' => $element, 'interval' => ($i - $pager_current), 'parameters' => $parameters)), + 'data' => theme('pager_next', array( + 'text' => $i, + 'element' => $element, + 'interval' => ($i - $pager_current), + 'parameters' => $parameters, + )), ); } } @@ -515,7 +652,13 @@ function theme_ting_search_pager($variables) { 'data' => $li_last, ); } - return theme('item_list', array('items' => $items, 'type' => 'ul', 'attributes' => array('class' => array('pager')))); + return theme('item_list', array( + 'items' => $items, + 'type' => 'ul', + 'attributes' => array( + 'class' => array('pager'), + ), + )); } } @@ -526,32 +669,36 @@ function theme_ting_search_mini_pager($variables) { $tags = $variables['tags']; $element = $variables['element']; $parameters = $variables['parameters']; - $quantity = $variables['quantity']; - global $pager_page_array, $pager_total; - - // Calculate various markers within this pager piece: - // Middle is used to "center" pages around the current page. - $pager_middle = ceil($quantity / 2); - // current is the page we are currently paged to - $pager_current = $pager_page_array[$element] + 1; - // max is the maximum page number - $pager_max = $pager_total[$element]; - // End of marker calculations. + global $pager_total; - - $li_previous = theme('pager_previous', array('text' => (isset($tags[1]) ? $tags[1] : t('‹ previous')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters)); + $li_previous = theme('pager_previous', array( + 'text' => isset($tags[1]) ? $tags[1] : t('‹ previous'), + 'element' => $element, + 'interval' => 1, + 'parameters' => $parameters, + )); if (empty($li_previous)) { $li_previous = " "; } - $li_first = theme('pager_first', array('text' => (isset($tags[0]) ? $tags[0] : t('« first')), 'element' => $element, 'parameters' => $parameters)); + $li_first = theme('pager_first', array( + 'text' => isset($tags[0]) ? $tags[0] : t('« first'), + 'element' => $element, + 'parameters' => $parameters, + )); if (empty($li_first)) { $li_first = " "; } - $li_next = theme('pager_next', array('text' => (isset($tags[3]) ? $tags[3] : t('next ›')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters)); + $li_next = theme('pager_next', array( + 'text' => isset($tags[3]) ? $tags[3] : t('next ›'), + 'element' => $element, + 'interval' => 1, + 'parameters' => $parameters, + )); + if (empty($li_next)) { $li_next = " "; } @@ -571,7 +718,13 @@ function theme_ting_search_mini_pager($variables) { 'class' => array('pager-next'), 'data' => $li_next, ); - return theme('item_list', array('items' => $items, 'type' => 'ul', 'attributes' => array('class' => array('pager')))); + return theme('item_list', array( + 'items' => $items, + 'type' => 'ul', + 'attributes' => array( + 'class' => array('pager'), + ), + )); } } @@ -580,19 +733,16 @@ function theme_ting_search_mini_pager($variables) { * Implements hook_block_info(). */ function ting_search_block_info() { - $blocks['sort-form'] = array( + return array( + 'sort_form' => array( 'info' => t('Ting search "sort by" form'), 'cache' => DRUPAL_CACHE_PER_PAGE, - ); - $blocks['records-per-page'] = array( - 'info' => t('Ting search "records per page"'), - 'cache' => DRUPAL_CACHE_PER_PAGE, - ); - $blocks['search-display-extended-query'] = array( + ), + 'search_display_extended_query' => array( 'info' => t('Ting search extended query display'), 'cache' => DRUPAL_CACHE_PER_PAGE, - ); - return $blocks; + ), + ); } /** @@ -600,23 +750,22 @@ function ting_search_block_info() { */ function ting_search_block_view($delta = '') { $block = array(); + switch ($delta) { - case 'sort-form': - drupal_add_css(drupal_get_path('module', 'ting_search') . '/ting_search_extendform.css'); - drupal_add_js(drupal_get_path('module', 'ting_search') . '/ting_search_extendform.js'); + case 'sort_form': + drupal_add_js(drupal_get_path('module', 'ting_search') . '/js/ting_search_extendform.js'); $block['subject'] = t('Ting search sort controls'); - $block['content'] = drupal_get_form('ting_search_sort_form'); - break; - case 'records-per-page': - drupal_add_css(drupal_get_path('module', 'ting_search') . '/ting_search_extendform.css'); - drupal_add_js(drupal_get_path('module', 'ting_search') . '/ting_search_extendform.js'); - $block['subject'] = t('Ting search records per page controls'); - $block['content'] = records_per_page_controls_content() ; + + // Only display form if there are any search results. + $search_result = drupal_static('ting_search_results'); + if (isset($search_result->numTotalObjects) && $search_result->numTotalObjects > 0) { + $block['content'] = drupal_get_form('ting_search_sort_form'); + } break; - case 'search-display-extended-query': - drupal_add_css(drupal_get_path('module', 'ting_search') . '/ting_search_extendform.css'); - drupal_add_js(drupal_get_path('module', 'ting_search') . '/ting_search_extendform.js'); - $block['content'] = theme('ting_search_display_extended_query', array('query_label'=>t('Your query:'),'query_string'=>NULL)); + + case 'search_display_extended_query': + drupal_add_js(drupal_get_path('module', 'ting_search') . '/js/ting_search_extendform.js'); + $block['content'] = theme('ting_search_display_extended_query', array('query_label' => t('Your query:'), 'query_string' => NULL)); break; } return $block; @@ -628,36 +777,53 @@ function search_extend_content() { } /** - * Create form for sorting search result + * Create form for sorting search result. */ function ting_search_sort_form($form_state) { + $form = array(); + $form['sort'] = array( - '#title' => t('Sort by:'), '#type' => 'select', - '#default_value' => isset($_GET['sort']) ? check_plain($_GET['sort']) : '', - '#options' => array( - '' => t('Ranking'), - 'title_ascending' => t('Title (Ascending)'), - 'title_descending' => t('Title (Descending)'), - 'creator_ascending' => t('Creator (Ascending)'), - 'creator_descending' => t('Creator (Descending)'), - 'date_ascending' => t('Date (Ascending)'), - 'date_descending' => t('Date (Descending)') - ), - '#description' => t('Set sort order for search result'), + '#default_value' => isset($_GET['sort']) ? check_plain($_GET['sort']) : variable_get('ting_search_default_sort', ''), + '#options' => ting_search_sort_options(), ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Sort'), '#states' => array( - 'visible' => array(':input[name="op"]' => array('value' => '', ), ), + 'visible' => array(':input[name="op"]' => array('value' => '')), ), ); return $form; } /** - * Process submitted data for sorting order form + * Set default sort options. + * + * @return array + * Returns an array of sort options. + */ +function ting_search_sort_options() { + $options = array( + '' => t('Ranking'), + 'title_ascending' => t('Title (Ascending)'), + 'title_descending' => t('Title (Descending)'), + 'creator_ascending' => t('Creator (Ascending)'), + 'creator_descending' => t('Creator (Descending)'), + 'date_ascending' => t('Date (Ascending)'), + 'date_descending' => t('Date (Descending)'), + ); + + // Add label to the front of the options. + foreach ($options as $key => $option) { + $options[$key] = t('Sort by: !sort', array('!sort' => $option)); + } + + return $options; +} + +/** + * Process submitted data for sorting order form. */ function ting_search_sort_form_submit($form, &$form_state) { $query = drupal_get_query_parameters(); @@ -666,39 +832,66 @@ function ting_search_sort_form_submit($form, &$form_state) { $query = array('sort' => $form_state['input']['sort']) + $query; } - $form_state['redirect'] = array($_GET['q'], array('query' => $query, ), ); - + $form_state['redirect'] = array($_GET['q'], array('query' => $query)); } /** - * Create links for changing how many records per page + * Create links for changing how many records per page. + * + * This function uses the static variable "ting_search_results", so if a search + * result have not been render yet it will be empty. So you have to ensure that + * the search have been executed. + * + * In panels "render last" option can help in the plugin array. */ -function records_per_page_controls_content() { - $sizes = array('10' => t('10'), '25' => t('25'), '50' => t('50'),); - $size = array(); - $size['#type'] = 'markup'; - $size['#prefix'] = '
' . t('Results on page:'); - $size['#suffix'] = '
'; - $size['#markup'] = ''; - - foreach ($sizes as $number => $text) { - $pg = array('page' => 0); - $sz = array('size' => $number); - $classes = array(); - $keys = array_keys($sizes); - - if (isset($_GET['size']) && $_GET['size'] == $number) { - $classes += array('selected'); - } - elseif ((!isset($_GET['size']) || - !in_array($_GET['size'], $sizes)) && - $number == $keys[0]) { - $classes += array('selected'); +function ting_search_records_per_page_controls_content() { + // Get search results. + $search_result = drupal_static('ting_search_results'); + + // Use static as panels may call this more than once. + $output = &drupal_static(__FUNCTION__, array()); + + // Don't show anything if the search result is empty. + if (!empty($search_result->collections) && empty($output)) { + // Get the default results per page. + $def_size = variable_get('ting_search_results_per_page', 10); + + // Set default as the first value. + $sizes = array($def_size => $def_size); + + // Increment the default size by 2.5 and 5 to provide the user with three + // different options. Defaults to 10, 25, 50 posts per search page. + $size = (int) round($def_size * 2.5); + $sizes[$size] = $size; + $size = (int) round($def_size * 5); + $sizes[$size] = $size; + + // Iterate through sizes and build the options. + foreach ($sizes as $number => $text) { + // Set the current pager page to first page (used in query). + $pager_page = array('page' => 0); + + // Find the currently select size. + $keys = array_keys($sizes); + $selected = FALSE; + if (isset($_GET['size']) && $_GET['size'] == $number) { + $selected = TRUE; + } + elseif (!isset($_GET['size']) && $number == $keys[0]) { + $selected = TRUE; + } + + // Add the result to the output. + $output[$text] = array( + 'size' => $number, + 'text' => $text, + 'query' => array('size' => $number) + $pager_page + drupal_get_query_parameters(), + 'selected' => $selected, + ); } - $size['#markup'] .= l($text, $_GET['q'], array('query' => $sz + $pg + drupal_get_query_parameters(), 'attributes' => array('class' => $classes, ), )); - } - return $size; + return $output; + } } /** @@ -714,14 +907,14 @@ function ting_search_submit($form, &$form_state) { unset($_GET['destination']); } - $form_id = $form['form_id']['#value']; // 'search_block_form' + $form_id = $form['form_id']['#value']; $keys = $form_state['values'][$form_id]; $fields = array(); $extended_fields = array( - 'creator' => 'dc.creator', - 'title' => 'dc.title', - 'subject' => 'dc.subject', + 'creator' => 'term.creator', + 'title' => 'term.title', + 'subject' => 'term.subject', ); foreach ($extended_fields as $name => $index) { @@ -738,15 +931,15 @@ function ting_search_submit($form, &$form_state) { if (!empty($fields)) { $q[] = '(' . implode(' AND ', $fields) . ')'; } - $q = join(' AND ', $q); + $q = implode(' AND ', $q); $s = $form_state['values']['sort']; - if ( $s != "" ) { + if ($s != "") { $controls['sort'] = $s; } $s = $form_state['values']['size']; - if ( $s != "" ) { + if ($s != "") { $controls['size'] = $s; } @@ -776,7 +969,7 @@ function ting_search_submit($form, &$form_state) { } if (!empty($search_info['path']) && in_array($search_info['module'], variable_get('search_active_modules', array()))) { $form_state['redirect'] = FALSE; - $url = 'search/' . $search_info['path']. '/' . trim($q); + $url = 'search/' . $search_info['path'] . '/' . trim($q); drupal_goto($url, array('query' => $controls)); } else { @@ -785,36 +978,9 @@ function ting_search_submit($form, &$form_state) { } /** - * Attempt to quote reserved words in a search query. - * - * As proper quoting would require a full CQL parser, we cheat and - * just work on the part we know is the free text part. - * - * Also, we don't mess with uppercase reserved words. + * Make sure string is strict cql */ function _ting_search_quote($string) { - if (preg_match('/^(.*?)(AND \(.*|$)/', $string, $rx)) { - $keys = $rx[1]; - $new_keys = preg_replace_callback('/(?:(".*?(?string_to_cql($string); } diff --git a/ting_search.pages.inc b/ting_search.pages.inc index e45f5f6..5c32297 100644 --- a/ting_search.pages.inc +++ b/ting_search.pages.inc @@ -8,9 +8,10 @@ function template_preprocess_ting_search_results(&$variables) { if (!empty($variables['module'])) { $variables['module'] = check_plain($variables['module']); } + foreach ($variables['results'] as $result) { $variables['search_results'] .= theme('search_result', array('result' => $result, 'module' => $variables['module'])); } - $variables['pager'] = theme('ting_search_mini_pager', array('tags' => NULL)); + $variables['pager'] = theme('ting_search_pager', array('tags' => NULL)); } diff --git a/ting_search_extendform.css b/ting_search_extendform.css deleted file mode 100644 index 07d835f..0000000 --- a/ting_search_extendform.css +++ /dev/null @@ -1,25 +0,0 @@ - -#edit-extend-form label { - display: inline; - margin-right: 1em; -} - -.page-search .pane-ting-search-search-display-extended-query .pane-content { - padding-top: 0.5em; - padding-bottom: 0em; - clear: both; -} - -#search-query-display { - font-size: 0.8em; - padding-bottom: 0.2em; - padding-top: 0.2em; - border-top: 1px solid #E7E7E6; - clear: both; -} - -span#search-query-label { - margin-right: 0.5em; - font-weight: bold; -} -