first import 7.x-1.4

This commit is contained in:
bachy
2013-03-15 17:32:30 +01:00
commit 9cc5ba4cfa
68 changed files with 17899 additions and 0 deletions

View File

@@ -0,0 +1,262 @@
<?php
/**
* @file
* Display plugin for displaying the search facets in a block.
*/
/**
* Plugin class for displaying search facets in a block.
*/
class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
public function displays_exposed() {
return FALSE;
}
public function uses_exposed() {
return FALSE;
}
public function option_definition() {
$options = parent::option_definition();
$options['linked_path'] = array('default' => '');
$options['facet_field'] = '';
$options['hide_block'] = FALSE;
return $options;
}
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
return;
}
switch ($form_state['section']) {
case 'linked_path':
$form['#title'] .= t('Search page path');
$form['linked_path'] = array(
'#type' => 'textfield',
'#description' => t('The menu path to which search facets will link. Leave empty to use the current path.'),
'#default_value' => $this->get_option('linked_path'),
);
break;
case 'facet_field':
$form['facet_field'] = array(
'#type' => 'select',
'#title' => t('Facet field'),
'#options' => $this->getFieldOptions(),
'#default_value' => $this->get_option('facet_field'),
);
break;
case 'use_more':
$form['use_more']['#description'] = t('This will add a more link to the bottom of this view, which will link to the base path for the facet links.');
$form['use_more_always'] = array(
'#type' => 'value',
'#value' => $this->get_option('use_more_always'),
);
break;
case 'hide_block':
$form['hide_block'] = array(
'#type' => 'checkbox',
'#title' => t('Hide block'),
'#description' => t('Hide this block, but still execute the search. ' .
'Can be used to show native Facet API facet blocks linking to the search page specified above.'),
'#default_value' => $this->get_option('hide_block'),
);
break;
}
}
public function options_validate(&$form, &$form_state) {
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
}
}
public function options_submit(&$form, &$form_state) {
parent::options_submit($form, $form_state);
switch ($form_state['section']) {
case 'linked_path':
$this->set_option('linked_path', $form_state['values']['linked_path']);
break;
case 'facet_field':
$this->set_option('facet_field', $form_state['values']['facet_field']);
break;
case 'hide_block':
$this->set_option('hide_block', $form_state['values']['hide_block']);
break;
}
}
public function options_summary(&$categories, &$options) {
parent::options_summary($categories, $options);
$options['linked_path'] = array(
'category' => 'block',
'title' => t('Search page path'),
'value' => $this->get_option('linked_path') ? $this->get_option('linked_path') : t('Use current path'),
);
$field_options = $this->getFieldOptions();
$options['facet_field'] = array(
'category' => 'block',
'title' => t('Facet field'),
'value' => $this->get_option('facet_field') ? $field_options[$this->get_option('facet_field')] : t('None'),
);
$options['hide_block'] = array(
'category' => 'block',
'title' => t('Hide block'),
'value' => $this->get_option('hide_block') ? t('Yes') : t('No'),
);
}
protected $field_options = NULL;
protected function getFieldOptions() {
if (!isset($this->field_options)) {
$index_id = substr($this->view->base_table, 17);
if (!($index_id && ($index = search_api_index_load($index_id)))) {
$table = views_fetch_data($this->view->base_table);
$table = empty($table['table']['base']['title']) ? $this->view->base_table : $table['table']['base']['title'];
throw new SearchApiException(t('The "Facets block" display cannot be used with a view for @basetable. ' .
'Please only use this display with base tables representing search indexes.',
array('@basetable' => $table)));
}
$this->field_options = array();
if (!empty($index->options['fields'])) {
foreach ($index->getFields() as $key => $field) {
$this->field_options[$key] = $field['name'];
}
}
}
return $this->field_options;
}
/**
* Render the 'more' link
*/
public function render_more_link() {
if ($this->use_more()) {
$path = $this->get_option('linked_path');
$theme = views_theme_functions('views_more', $this->view, $this->display);
$path = check_url(url($path, array()));
return array(
'#theme' => $theme,
'#more_url' => $path,
'#link_text' => check_plain($this->use_more_text()),
);
}
}
public function execute() {
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
return NULL;
}
$facet_field = $this->get_option('facet_field');
if (!$facet_field) {
return NULL;
}
$base_path = $this->get_option('linked_path');
if (!$base_path) {
$base_path = $_GET['q'];
}
$this->view->build();
$limit = empty($this->view->query->pager->options['items_per_page']) ? 10 : $this->view->query->pager->options['items_per_page'];
$query_options = &$this->view->query->getOptions();
if (!$this->get_option('hide_block')) {
// If we hide the block, we don't need this extra facet.
$query_options['search_api_facets']['search_api_views_facets_block'] = array(
'field' => $facet_field,
'limit' => $limit,
'missing' => FALSE,
'min_count' => 1,
);
}
$query_options['search id'] = 'search_api_views:' . $this->view->name . '-facets_block';
$query_options['search_api_base_path'] = $base_path;
$this->view->query->range(0, 0);
$this->view->execute();
if ($this->get_option('hide_block')) {
return NULL;
}
$results = $this->view->query->getSearchApiResults();
if (empty($results['search_api_facets']['search_api_views_facets_block'])) {
return NULL;
}
$terms = $results['search_api_facets']['search_api_views_facets_block'];
$filters = array();
foreach ($terms as $term) {
$filter = $term['filter'];
if ($filter[0] == '"') {
$filter = substr($filter, 1, -1);
}
elseif ($filter != '!') {
// This is a range filter.
$filter = substr($filter, 1, -1);
$pos = strpos($filter, ' ');
if ($pos !== FALSE) {
$filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
}
}
$filters[$term['filter']] = $filter;
}
$index = $this->view->query->getIndex();
$options['field'] = $index->options['fields'][$facet_field];
$options['field']['key'] = $facet_field;
$options['index id'] = $index->machine_name;
$options['value callback'] = '_search_api_facetapi_facet_create_label';
$map = search_api_facetapi_facet_map_callback($filters, $options);
$facets = array();
$prefix = rawurlencode($facet_field) . ':';
foreach ($terms as $term) {
$name = $filter = $filters[$term['filter']];
if (isset($map[$filter])) {
$name = $map[$filter];
}
$query['f'][0] = $prefix . $filter;
// Initializes variables passed to theme hook.
$variables = array(
'text' => $name,
'path' => $base_path,
'count' => $term['count'],
'options' => array(
'attributes' => array('class' => 'facetapi-inactive'),
'html' => FALSE,
'query' => $query,
),
);
// Themes the link, adds row to facets.
$facets[] = array(
'class' => array('leaf'),
'data' => theme('facetapi_link_inactive', $variables),
);
}
if (!$facets) {
return NULL;
}
$info['content']['facets'] = array(
'#theme' => 'item_list',
'#items' => $facets,
);
$info['content']['more'] = $this->render_more_link();
$info['subject'] = filter_xss_admin($this->view->get_title());
return $info;
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* Views argument handler class for handling all non-fulltext types.
*/
class SearchApiViewsHandlerArgument extends views_handler_argument {
/**
* The associated views query object.
*
* @var SearchApiViewsQuery
*/
public $query;
/**
* Determine if the argument can generate a breadcrumb
*
* @return boolean
*/
// @todo Change and implement set_breadcrumb()?
public function uses_breadcrumb() {
return FALSE;
}
/**
* Provide a list of default behaviors for this argument if the argument
* is not present.
*
* Override this method to provide additional (or fewer) default behaviors.
*/
public function default_actions($which = NULL) {
$defaults = array(
'ignore' => array(
'title' => t('Display all values'),
'method' => 'default_ignore',
'breadcrumb' => TRUE, // generate a breadcrumb to here
),
'not found' => array(
'title' => t('Hide view / Page not found (404)'),
'method' => 'default_not_found',
'hard fail' => TRUE, // This is a hard fail condition
),
'empty' => array(
'title' => t('Display empty text'),
'method' => 'default_empty',
'breadcrumb' => TRUE, // generate a breadcrumb to here
),
'default' => array(
'title' => t('Provide default argument'),
'method' => 'default_default',
'form method' => 'default_argument_form',
'has default argument' => TRUE,
'default only' => TRUE, // this can only be used for missing argument, not validation failure
),
);
if ($which) {
return isset($defaults[$which]) ? $defaults[$which] : NULL;
}
return $defaults;
}
public function option_definition() {
$options = parent::option_definition();
$options['break_phrase'] = array('default' => FALSE);
return $options;
}
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
// Allow passing multiple values.
$form['break_phrase'] = array(
'#type' => 'checkbox',
'#title' => t('Allow multiple values'),
'#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'),
'#default_value' => $this->options['break_phrase'],
'#fieldset' => 'more',
);
}
/**
* Set up the query for this argument.
*
* The argument sent may be found at $this->argument.
*/
// @todo Provide options to select the operator, instead of always using '='?
public function query($group_by = FALSE) {
if (!empty($this->options['break_phrase'])) {
views_break_phrase($this->argument, $this);
}
else {
$this->value = array($this->argument);
}
if (count($this->value) > 1) {
$filter = $this->query->createFilter(drupal_strtoupper($this->operator));
// $filter will be NULL if there were errors in the query.
if ($filter) {
foreach ($this->value as $value) {
$filter->condition($this->real_field, $value, '=');
}
$this->query->filter($filter);
}
}
else {
$this->query->condition($this->real_field, reset($this->value));
}
}
/**
* Get the title this argument will assign the view, given the argument.
*
* This usually needs to be overridden to provide a proper title.
*/
public function title() {
return t('Search @field for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument));
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* Views argument handler class for handling fulltext fields.
*/
class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumentText {
/**
* Specify the options this filter uses.
*/
public function option_definition() {
$options = parent::option_definition();
$options['fields'] = array('default' => array());
return $options;
}
/**
* Extend the options form a bit.
*/
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$fields = $this->getFulltextFields();
if (!empty($fields)) {
$form['fields'] = array(
'#type' => 'select',
'#title' => t('Searched fields'),
'#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
'#options' => $fields,
'#size' => min(4, count($fields)),
'#multiple' => TRUE,
'#default_value' => $this->options['fields'],
);
}
else {
$form['fields'] = array(
'#type' => 'value',
'#value' => array(),
);
}
}
/**
* Set up the query for this argument.
*
* The argument sent may be found at $this->argument.
*/
public function query($group_by = FALSE) {
if ($this->options['fields']) {
$this->query->fields($this->options['fields']);
}
$old = $this->query->getOriginalKeys();
$this->query->keys($this->argument);
if ($old) {
$keys = &$this->query->getKeys();
if (is_array($keys)) {
$keys[] = $old;
}
elseif (is_array($old)) {
// We don't support such nonsense.
}
else {
$keys = "($old) ($keys)";
}
}
}
/**
* Helper method to get an option list of all available fulltext fields.
*/
protected function getFulltextFields() {
$ret = array();
$index = search_api_index_load(substr($this->table, 17));
if (!empty($index->options['fields'])) {
$fields = $index->getFields();
foreach ($index->getFulltextFields() as $field) {
$ret[$field] = $fields[$field]['name'];
}
}
return $ret;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* Views argument handler providing a list of related items for search servers
* supporting the "search_api_mlt" feature.
*/
class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArgument {
/**
* Specify the options this filter uses.
*/
public function option_definition() {
$options = parent::option_definition();
unset($options['break_phrase']);
$options['fields'] = array('default' => array());
return $options;
}
/**
* Extend the options form a bit.
*/
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
unset($form['break_phrase']);
$index = search_api_index_load(substr($this->table, 17));
if (!empty($index->options['fields'])) {
$fields = array();
foreach ($index->getFields() as $key => $field) {
$fields[$key] = $field['name'];
}
}
if (!empty($fields)) {
$form['fields'] = array(
'#type' => 'select',
'#title' => t('Fields for Similarity'),
'#description' => t('Select the fields that will be used for finding similar content. If no fields are selected, all available fields will be used.'),
'#options' => $fields,
'#size' => min(8, count($fields)),
'#multiple' => TRUE,
'#default_value' => $this->options['fields'],
);
}
else {
$form['fields'] = array(
'#type' => 'value',
'#value' => array(),
);
}
}
/**
* Set up the query for this argument.
*
* The argument sent may be found at $this->argument.
*/
public function query($group_by = FALSE) {
$server = $this->query->getIndex()->server();
if (!$server->supportsFeature('search_api_mlt')) {
$class = search_api_get_service_info($server->class);
throw new SearchApiException(t('The search service "@class" does not offer "More like this" functionality.',
array('@class' => $class['name'])));
return;
}
$fields = $this->options['fields'] ? $this->options['fields'] : array();
if (empty($fields)) {
foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
$fields[] = $key;
}
}
$mlt = array(
'id' => $this->argument,
'fields' => $fields,
);
$this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* Views argument handler class for handling fulltext fields.
*/
class SearchApiViewsHandlerArgumentText extends SearchApiViewsHandlerArgument {
/**
* Get the title this argument will assign the view, given the argument.
*
* This usually needs to be overridden to provide a proper title.
*/
public function title() {
return t('Search for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument));
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Views filter handler base class for handling all "normal" cases.
*/
class SearchApiViewsHandlerFilter extends views_handler_filter {
/**
* The value to filter for.
*
* @var mixed
*/
public $value;
/**
* The operator used for filtering.
*
* @var string
*/
public $operator;
/**
* The associated views query object.
*
* @var SearchApiViewsQuery
*/
public $query;
/**
* Provide a list of options for the operator form.
*/
public function operator_options() {
return array(
'<' => t('Is smaller than'),
'<=' => t('Is smaller than or equal to'),
'=' => t('Is equal to'),
'<>' => t('Is not equal to'),
'>=' => t('Is greater than or equal to'),
'>' => t('Is greater than'),
'empty' => t('Is empty'),
'not empty' => t('Is not empty'),
);
}
/**
* Provide a form for setting the filter value.
*/
public function value_form(&$form, &$form_state) {
while (is_array($this->value)) {
$this->value = $this->value ? array_shift($this->value) : NULL;
}
$form['value'] = array(
'#type' => 'textfield',
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#size' => 30,
'#default_value' => isset($this->value) ? $this->value : '',
);
// Hide the value box if the operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
$form['value']['#states']['visible'] = array(
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
);
}
/**
* Display the filter on the administrative summary
*/
function admin_summary() {
if (!empty($this->options['exposed'])) {
return t('exposed');
}
if ($this->operator === 'empty') {
return t('is empty');
}
if ($this->operator === 'not empty') {
return t('is not empty');
}
return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value);
}
/**
* Add this filter to the query.
*/
public function query() {
if ($this->operator === 'empty') {
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
}
elseif ($this->operator === 'not empty') {
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
}
else {
while (is_array($this->value)) {
$this->value = $this->value ? reset($this->value) : NULL;
}
if (strlen($this->value) > 0) {
$this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Views filter handler class for handling fulltext fields.
*/
class SearchApiViewsHandlerFilterBoolean extends SearchApiViewsHandlerFilter {
/**
* Provide a list of options for the operator form.
*/
public function operator_options() {
return array();
}
/**
* Provide a form for setting the filter value.
*/
public function value_form(&$form, &$form_state) {
while (is_array($this->value)) {
$this->value = $this->value ? array_shift($this->value) : NULL;
}
$form['value'] = array(
'#type' => 'select',
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#options' => array(1 => t('True'), 0 => t('False')),
'#default_value' => isset($this->value) ? $this->value : '',
);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* Views filter handler base class for handling all "normal" cases.
*/
class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
/**
* Add a "widget type" option.
*/
public function option_definition() {
return parent::option_definition() + array(
'widget_type' => array('default' => 'default'),
);
}
/**
* If the date popup module is enabled, provide the extra option setting.
*/
public function has_extra_options() {
if (module_exists('date_popup')) {
return TRUE;
}
return FALSE;
}
/**
* Add extra options if we allow the date popup widget.
*/
public function extra_options_form(&$form, &$form_state) {
parent::extra_options_form($form, $form_state);
if (module_exists('date_popup')) {
$widget_options = array('default' => 'Default', 'date_popup' => 'Date popup');
$form['widget_type'] = array(
'#type' => 'radios',
'#title' => t('Date selection form element'),
'#default_value' => $this->options['widget_type'],
'#options' => $widget_options,
);
}
}
/**
* Provide a form for setting the filter value.
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);
// If we are using the date popup widget, overwrite the settings of the form
// according to what date_popup expects.
if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) {
$form['value']['#type'] = 'date_popup';
$form['value']['#date_format'] = 'm/d/Y';
unset($form['value']['#description']);
}
elseif (empty($form_state['exposed'])) {
$form['value']['#description'] = t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "@date1" or "@date2".', array(
'@doc-link' => 'http://php.net/manual/en/function.strtotime.php',
'@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'),
'@date2' => 'now + 1 day',
));
}
}
/**
* Add this filter to the query.
*/
public function query() {
if ($this->operator === 'empty') {
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
}
elseif ($this->operator === 'not empty') {
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
}
else {
while (is_array($this->value)) {
$this->value = $this->value ? reset($this->value) : NULL;
}
$v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
if ($v !== FALSE) {
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
}
}
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* Views filter handler class for handling fulltext fields.
*/
class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterText {
/**
* Specify the options this filter uses.
*/
public function option_definition() {
$options = parent::option_definition();
$options['mode'] = array('default' => 'keys');
$options['fields'] = array('default' => array());
return $options;
}
/**
* Extend the options form a bit.
*/
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$form['mode'] = array(
'#title' => t('Use as'),
'#type' => 'radios',
'#options' => array(
'keys' => t('Search keys multiple words will be split and the filter will influence relevance.'),
'filter' => t("Search filter use as a single phrase that restricts the result set but doesn't influence relevance."),
),
'#default_value' => $this->options['mode'],
);
$fields = $this->getFulltextFields();
if (!empty($fields)) {
$form['fields'] = array(
'#type' => 'select',
'#title' => t('Searched fields'),
'#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
'#options' => $fields,
'#size' => min(4, count($fields)),
'#multiple' => TRUE,
'#default_value' => $this->options['fields'],
);
}
else {
$form['fields'] = array(
'#type' => 'value',
'#value' => array(),
);
}
if (isset($form['expose'])) {
$form['expose']['#weight'] = -5;
}
}
/**
* Add this filter to the query.
*/
public function query() {
while (is_array($this->value)) {
$this->value = $this->value ? reset($this->value) : '';
}
// Catch empty strings entered by the user, but not "0".
if ($this->value === '') {
return;
}
$fields = $this->options['fields'];
$fields = $fields ? $fields : array_keys($this->getFulltextFields());
// If something already specifically set different fields, we silently fall
// back to mere filtering.
$filter = $this->options['mode'] == 'filter';
if (!$filter) {
$old = $this->query->getFields();
$filter = $old && (array_diff($old, $fields) || array_diff($fields, $old));
}
if ($filter) {
$filter = $this->query->createFilter('OR');
foreach ($fields as $field) {
$filter->condition($field, $this->value, $this->operator);
}
$this->query->filter($filter);
return;
}
$this->query->fields($fields);
$old = $this->query->getOriginalKeys();
$this->query->keys($this->value);
if ($this->operator != '=') {
$keys = &$this->query->getKeys();
if (is_array($keys)) {
$keys['#negation'] = TRUE;
}
else {
// We can't know how negation is expressed in the server's syntax.
}
}
if ($old) {
$keys = &$this->query->getKeys();
if (is_array($keys)) {
$keys[] = $old;
}
elseif (is_array($old)) {
// We don't support such nonsense.
}
else {
$keys = "($old) ($keys)";
}
}
}
/**
* Helper method to get an option list of all available fulltext fields.
*/
protected function getFulltextFields() {
$fields = array();
$index = search_api_index_load(substr($this->table, 17));
if (!empty($index->options['fields'])) {
$f = $index->getFields();
foreach ($index->getFulltextFields() as $name) {
$fields[$name] = $f[$name]['name'];
}
}
return $fields;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains the SearchApiViewsHandlerFilterLanguage class.
*/
/**
* Views filter handler class for handling the special "Item language" field.
*
* Definition items:
* - options: An array of possible values for this field.
*/
class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions {
/**
* Provide a form for setting options.
*/
public function value_form(&$form, &$form_state) {
parent::value_form($form, $form_state);
$form['value']['#options'] = array(
'current' => t("Current user's language"),
'default' => t('Default site language'),
) + $form['value']['#options'];
}
/**
* Provides a summary of this filter's value for the admin UI.
*/
public function admin_summary() {
$tmp = $this->definition['options'];
$this->definition['options']['current'] = t('current');
$this->definition['options']['default'] = t('default');
$ret = parent::admin_summary();
$this->definition['options'] = $tmp;
return $ret;
}
/**
* Add this filter to the query.
*/
public function query() {
global $language_content;
foreach ($this->value as $i => $v) {
if ($v == 'current') {
$this->value[$i] = $language_content->language;
}
elseif ($v == 'default') {
$this->value[$i] = language_default('language');
}
}
parent::query();
}
}

View File

@@ -0,0 +1,205 @@
<?php
/**
* Views filter handler class for handling fields with a limited set of possible
* values.
*
* Definition items:
* - options: An array of possible values for this field.
*/
class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
protected $value_form_type = 'checkboxes';
/**
* Provide a list of options for the operator form.
*/
public function operator_options() {
return array(
'=' => t('Is one of'),
'<>' => t('Is not one of'),
'empty' => t('Is empty'),
'not empty' => t('Is not empty'),
);
}
/**
* Set "reduce" option to FALSE by default.
*/
public function expose_options() {
parent::expose_options();
$this->options['expose']['reduce'] = FALSE;
}
/**
* Add the "reduce" option to the exposed form.
*/
public function expose_form(&$form, &$form_state) {
parent::expose_form($form, $form_state);
$form['expose']['reduce'] = array(
'#type' => 'checkbox',
'#title' => t('Limit list to selected items'),
'#description' => t('If checked, the only items presented to the user will be the ones selected here.'),
'#default_value' => !empty($this->options['expose']['reduce']),
);
}
/**
* Define "reduce" option.
*/
public function option_definition() {
$options = parent::option_definition();
$options['expose']['contains']['reduce'] = array('default' => FALSE);
return $options;
}
/**
* Reduce the options according to the selection.
*/
protected function reduce_value_options() {
$options = array();
foreach ($this->definition['options'] as $id => $option) {
if (isset($this->options['value'][$id])) {
$options[$id] = $option;
}
}
return $options;
}
/**
* Save set checkboxes.
*/
public function value_submit($form, &$form_state) {
// Drupal's FAPI system automatically puts '0' in for any checkbox that
// was not set, and the key to the checkbox if it is set.
// Unfortunately, this means that if the key to that checkbox is 0,
// we are unable to tell if that checkbox was set or not.
// Luckily, the '#value' on the checkboxes form actually contains
// *only* a list of checkboxes that were set, and we can use that
// instead.
$form_state['values']['options']['value'] = $form['value']['#value'];
}
/**
* Provide a form for setting options.
*/
public function value_form(&$form, &$form_state) {
$options = array();
if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
$options += $this->reduce_value_options($form_state);
}
else {
$options += $this->definition['options'];
}
$form['value'] = array(
'#type' => $this->value_form_type,
'#title' => empty($form_state['exposed']) ? t('Value') : '',
'#options' => $options,
'#multiple' => TRUE,
'#size' => min(4, count($this->definition['options'])),
'#default_value' => isset($this->value) ? $this->value : array(),
);
// Hide the value box if operator is 'empty' or 'not empty'.
// Radios share the same selector so we have to add some dummy selector.
// #states replace #dependency (http://drupal.org/node/1595022).
$form['value']['#states']['visible'] = array(
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
);
}
/**
* Provides a summary of this filter's value for the admin UI.
*/
public function admin_summary() {
if (!empty($this->options['exposed'])) {
return t('exposed');
}
if ($this->operator === 'empty') {
return t('is empty');
}
if ($this->operator === 'not empty') {
return t('is not empty');
}
if (!is_array($this->value)) {
return;
}
$operator_options = $this->operator_options();
$operator = $operator_options[$this->operator];
$values = '';
// Remove every element which is not known.
foreach ($this->value as $i => $value) {
if (!isset($this->definition['options'][$value])) {
unset($this->value[$i]);
}
}
// Choose different kind of ouput for 0, a single and multiple values.
if (count($this->value) == 0) {
return $this->operator == '=' ? t('none') : t('any');
}
elseif (count($this->value) == 1) {
// If there is only a single value, use just the plain operator, = or <>.
$operator = check_plain($this->operator);
$values = check_plain($this->definition['options'][reset($this->value)]);
}
else {
foreach ($this->value as $value) {
if ($values !== '') {
$values .= ', ';
}
if (drupal_strlen($values) > 20) {
$values .= '…';
break;
}
$values .= check_plain($this->definition['options'][$value]);
}
}
return $operator . (($values !== '') ? ' ' . $values : '');
}
/**
* Add this filter to the query.
*/
public function query() {
if ($this->operator === 'empty') {
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
}
elseif ($this->operator === 'not empty') {
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
}
else {
while (is_array($this->value) && count($this->value) == 1) {
$this->value = reset($this->value);
}
if (is_scalar($this->value) && $this->value !== '') {
$this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
}
elseif ($this->value) {
if ($this->operator == '=') {
$filter = $this->query->createFilter('OR');
// $filter will be NULL if there were errors in the query.
if ($filter) {
foreach ($this->value as $v) {
$filter->condition($this->real_field, $v, '=');
}
$this->query->filter($filter, $this->options['group']);
}
}
else {
foreach ($this->value as $v) {
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* Views filter handler class for handling fulltext fields.
*/
class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter {
/**
* Provide a list of options for the operator form.
*/
public function operator_options() {
return array('=' => t('contains'), '<>' => t("doesn't contain"));
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Class for sorting results according to a specified field.
*/
class SearchApiViewsHandlerSort extends views_handler_sort {
/**
* The associated views query object.
*
* @var SearchApiViewsQuery
*/
public $query;
/**
* Called to add the sort to a query.
*/
public function query() {
// When there are exposed sorts, the "exposed form" plugin will set
// $query->orderby to an empty array. Therefore, if that property is set,
// we here remove all previous sorts.
if (isset($this->query->orderby)) {
unset($this->query->orderby);
$sort = &$this->query->getSort();
$sort = array();
}
$this->query->sort($this->real_field, $this->options['order']);
}
}

View File

@@ -0,0 +1,564 @@
<?php
/**
* Views query class using a Search API index as the data source.
*/
class SearchApiViewsQuery extends views_plugin_query {
/**
* Number of results to display.
*
* @var int
*/
protected $limit;
/**
* Offset of first displayed result.
*
* @var int
*/
protected $offset;
/**
* The index this view accesses.
*
* @var SearchApiIndex
*/
protected $index;
/**
* The query that will be executed.
*
* @var SearchApiQueryInterface
*/
protected $query;
/**
* The results returned by the query, after it was executed.
*
* @var array
*/
protected $search_api_results = array();
/**
* Array of all encountered errors.
*
* Each of these is fatal, meaning that a non-empty $errors property will
* result in an empty result being returned.
*
* @var array
*/
protected $errors;
/**
* The names of all fields whose value is required by a handler.
*
* The format follows the same as Search API field identifiers (parent:child).
*
* @var array
*/
protected $fields;
/**
* The query's sub-filters representing the different Views filter groups.
*
* @var array
*/
protected $filters = array();
/**
* The conjunction with which multiple filter groups are combined.
*
* @var string
*/
public $group_operator = 'AND';
/**
* Create the basic query object and fill with default values.
*/
public function init($base_table, $base_field, $options) {
try {
$this->errors = array();
parent::init($base_table, $base_field, $options);
$this->fields = array();
if (substr($base_table, 0, 17) == 'search_api_index_') {
$id = substr($base_table, 17);
$this->index = search_api_index_load($id);
$this->query = $this->index->query(array(
'parse mode' => 'terms',
));
}
}
catch (Exception $e) {
$this->errors[] = $e->getMessage();
}
}
/**
* Add a field that should be retrieved from the results by this view.
*
* @param $field
* The field's identifier, as used by the Search API. E.g., "title" for a
* node's title, "author:name" for a node's author's name.
*
* @return SearchApiViewsQuery
* The called object.
*/
public function addField($field) {
$this->fields[$field] = TRUE;
return $field;
}
/**
* Add a sort to the query.
*
* @param $selector
* The field to sort on. All indexed fields of the index are valid values.
* In addition, the special fields 'search_api_relevance' (sort by
* relevance) and 'search_api_id' (sort by item id) may be used.
* @param $order
* The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
*/
public function add_selector_orderby($selector, $order = 'ASC') {
$this->query->sort($selector, $order);
}
/**
* Defines the options used by this query plugin.
*
* Adds an option to bypass access checks.
*/
public function option_definition() {
return parent::option_definition() + array(
'search_api_bypass_access' => array(
'default' => FALSE,
),
);
}
/**
* Add settings for the UI.
*
* Adds an option for bypassing access checks.
*/
public function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$form['search_api_bypass_access'] = array(
'#type' => 'checkbox',
'#title' => t('Bypass access checks'),
'#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
'#default_value' => $this->options['search_api_bypass_access'],
);
}
/**
* Builds the necessary info to execute the query.
*/
public function build(&$view) {
$this->view = $view;
// Setup the nested filter structure for this query.
if (!empty($this->where)) {
// If the different groups are combined with the OR operator, we have to
// add a new OR filter to the query to which the filters for the groups
// will be added.
if ($this->group_operator === 'OR') {
$base = $this->query->createFilter('OR');
$this->query->filter($base);
}
else {
$base = $this->query;
}
// Add a nested filter for each filter group, with its set conjunction.
foreach ($this->where as $group_id => $group) {
if (!empty($group['conditions']) || !empty($group['filters'])) {
// For filters without a group, we want to always add them directly to
// the query.
$filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
if (!empty($group['conditions'])) {
foreach ($group['conditions'] as $condition) {
list($field, $value, $operator) = $condition;
$filter->condition($field, $value, $operator);
}
}
if (!empty($group['filters'])) {
foreach ($group['filters'] as $nested_filter) {
$filter->filter($nested_filter);
}
}
// If no group was given, the filters were already set on the query.
if ($group_id !== '') {
$base->filter($filter);
}
}
}
}
// Initialize the pager and let it modify the query to add limits.
$view->init_pager();
$this->pager->query();
// Add the "search_api_bypass_access" option to the query, if desired.
if (!empty($this->options['search_api_bypass_access'])) {
$this->query->setOption('search_api_bypass_access', TRUE);
}
}
/**
* Executes the query and fills the associated view object with according
* values.
*
* Values to set: $view->result, $view->total_rows, $view->execute_time,
* $view->pager['current_page'].
*/
public function execute(&$view) {
if ($this->errors) {
if (error_displayable()) {
foreach ($this->errors as $msg) {
drupal_set_message(check_plain($msg), 'error');
}
}
$view->result = array();
$view->total_rows = 0;
$view->execute_time = 0;
return;
}
try {
$start = microtime(TRUE);
// Add range and search ID (if it wasn't already set).
$this->query->range($this->offset, $this->limit);
if ($this->query->getOption('search id') == get_class($this->query)) {
$this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
}
// Execute the search.
$results = $this->query->execute();
$this->search_api_results = $results;
// Store the results.
$this->pager->total_items = $view->total_rows = $results['result count'];
if (!empty($this->pager->options['offset'])) {
$this->pager->total_items -= $this->pager->options['offset'];
}
$this->pager->update_page_info();
$view->result = array();
if (!empty($results['results'])) {
$this->addResults($results['results'], $view);
}
// We shouldn't use $results['performance']['complete'] here, since
// extracting the results probably takes considerable time as well.
$view->execute_time = microtime(TRUE) - $start;
}
catch (Exception $e) {
$this->errors[] = $e->getMessage();
// Recursion to get the same error behaviour as above.
return $this->execute($view);
}
}
/**
* Helper function for adding results to a view in the format expected by the
* view.
*/
protected function addResults(array $results, $view) {
$rows = array();
$missing = array();
$items = array();
// First off, we try to gather as much field values as possible without
// loading any items.
foreach ($results as $id => $result) {
$row = array();
// Include the loaded item for this result row, if present, or the item
// ID.
if (!empty($result['entity'])) {
$row['entity'] = $result['entity'];
}
else {
$row['entity'] = $id;
}
$row['_entity_properties']['search_api_relevance'] = $result['score'];
$row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
// Gather any fields from the search results.
if (!empty($result['fields'])) {
$row['_entity_properties'] += $result['fields'];
}
// Check whether we need to extract any properties from the result item.
$missing_fields = array_diff_key($this->fields, $row);
if ($missing_fields) {
$missing[$id] = $missing_fields;
if (is_object($row['entity'])) {
$items[$id] = $row['entity'];
}
else {
$ids[] = $id;
}
}
// Save the row values for adding them to the Views result afterwards.
$rows[$id] = (object) $row;
}
// Load items of those rows which haven't got all field values, yet.
if (!empty($ids)) {
$items += $this->index->loadItems($ids);
// $items now includes loaded items, and those already passed in the
// search results.
foreach ($items as $id => $item) {
// Extract item properties.
$wrapper = $this->index->entityWrapper($item, FALSE);
$rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
$rows[$id]->entity = $item;
}
}
// Finally, add all rows to the Views result set.
$view->result = array_values($rows);
}
/**
* Helper function for extracting all necessary fields from a result item.
*
* Usually, this method isn't needed anymore as the properties are now
* extracted by the field handlers themselves.
*/
protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
$fields = array();
foreach ($all_fields as $key => $true) {
$fields[$key]['type'] = 'string';
}
$fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
$ret = array();
foreach ($all_fields as $key => $true) {
$ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
}
return $ret;
}
/**
* Returns the according entity objects for the given query results.
*
* This is necessary to support generic entity handlers and plugins with this
* query backend.
*
* If the current query isn't based on an entity type, the method will return
* an empty array.
*/
public function get_result_entities($results, $relationship = NULL, $field = NULL) {
list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field);
$return = array();
foreach ($wrappers as $id => $wrapper) {
try {
$return[$id] = $wrapper->value();
}
catch (EntityMetadataWrapperException $e) {
// Ignore.
}
}
return array($type, $return);
}
/**
* Returns the according metadata wrappers for the given query results.
*
* This is necessary to support generic entity handlers and plugins with this
* query backend.
*/
public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
$is_entity = (boolean) entity_get_info($this->index->item_type);
$wrappers = array();
$load_entities = array();
foreach ($results as $row_index => $row) {
if ($is_entity && isset($row->entity)) {
// If this entity isn't load, register it for pre-loading.
if (!is_object($row->entity)) {
$load_entities[$row->entity] = $row_index;
}
$wrappers[$row_index] = $this->index->entityWrapper($row->entity);
}
}
// If the results are entities, we pre-load them to make use of a multiple
// load. (Otherwise, each result would be loaded individually.)
if (!empty($load_entities)) {
$entities = entity_load($this->index->item_type, array_keys($load_entities));
foreach ($entities as $entity_id => $entity) {
$wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
}
}
// Apply the relationship, if necessary.
$type = $this->index->item_type;
$selector_suffix = '';
if ($field && ($pos = strrpos($field, ':'))) {
$selector_suffix = substr($field, 0, $pos);
}
if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
// Use EntityFieldHandlerHelper to compute the correct data selector for
// the relationship.
$handler = (object) array(
'view' => $this->view,
'relationship' => $relationship,
'real_field' => '',
);
$selector = EntityFieldHandlerHelper::construct_property_selector($handler);
$selector .= ($selector ? ':' : '') . $selector_suffix;
list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
}
return array($type, $wrappers);
}
/**
* API function for accessing the raw Search API query object.
*
* @return SearchApiQueryInterface
* The search query object used internally by this handler.
*/
public function getSearchApiQuery() {
return $this->query;
}
/**
* API function for accessing the raw Search API results.
*
* @return array
* An associative array containing the search results, as specified by
* SearchApiQueryInterface::execute().
*/
public function getSearchApiResults() {
return $this->search_api_results;
}
//
// Query interface methods (proxy to $this->query)
//
public function createFilter($conjunction = 'AND') {
if (!$this->errors) {
return $this->query->createFilter($conjunction);
}
}
public function keys($keys = NULL) {
if (!$this->errors) {
$this->query->keys($keys);
}
return $this;
}
public function fields(array $fields) {
if (!$this->errors) {
$this->query->fields($fields);
}
return $this;
}
/**
* Adds a nested filter to the search query object.
*
* If $group is given, the filter is added to the relevant filter group
* instead.
*/
public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
if (!$this->errors) {
$this->where[$group]['filters'][] = $filter;
}
return $this;
}
/**
* Set a condition on the search query object.
*
* If $group is given, the condition is added to the relevant filter group
* instead.
*/
public function condition($field, $value, $operator = '=', $group = NULL) {
if (!$this->errors) {
$this->where[$group]['conditions'][] = array($field, $value, $operator);
}
return $this;
}
public function sort($field, $order = 'ASC') {
if (!$this->errors) {
$this->query->sort($field, $order);
}
return $this;
}
public function range($offset = NULL, $limit = NULL) {
if (!$this->errors) {
$this->query->range($offset, $limit);
}
return $this;
}
public function getIndex() {
return $this->index;
}
public function &getKeys() {
if (!$this->errors) {
return $this->query->getKeys();
}
$ret = NULL;
return $ret;
}
public function getOriginalKeys() {
if (!$this->errors) {
return $this->query->getOriginalKeys();
}
}
public function &getFields() {
if (!$this->errors) {
return $this->query->getFields();
}
$ret = NULL;
return $ret;
}
public function getFilter() {
if (!$this->errors) {
return $this->query->getFilter();
}
}
public function &getSort() {
if (!$this->errors) {
return $this->query->getSort();
}
$ret = NULL;
return $ret;
}
public function getOption($name) {
if (!$this->errors) {
return $this->query->getOption($name);
}
}
public function setOption($name, $value) {
if (!$this->errors) {
return $this->query->setOption($name, $value);
}
}
public function &getOptions() {
if (!$this->errors) {
return $this->query->getOptions();
}
$ret = NULL;
return $ret;
}
}