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,71 @@
Search API Views integration
----------------------------
This module integrates the Search API with the popular Views module [1],
allowing users to create views with filters, arguments, sorts and fields based
on any search index.
[1] http://drupal.org/project/views
"More like this" feature
------------------------
This module defines the "More like this" feature (feature key: "search_api_mlt")
that search service classes can implement. With a server supporting this, you
can use the „More like this“ contextual filter to display a list of items
related to a given item (usually, nodes similar to the node currently viewed).
For developers:
A service class that wants to support this feature has to check for a
"search_api_mlt" option in the search() method. When present, it will be an
array containing two keys:
- id: The entity ID of the item to which related items should be searched.
- fields: An array of indexed fields to use for testing the similarity of items.
When these are present, the normal keywords should be ignored and the related
items be returned as results instead. Sorting, filtering and range restriction
should all work normally.
"Facets block" display
----------------------
Most features should be clear to users of Views. However, the module also
provides a new display type, "Facets block", that might need some explanation.
This display type is only available, if the „Search facets“ module is also
enabled.
The basic use of the block is to provide a list of links to the most popular
filter terms (i.e., the ones with the most results) for a certain category. For
example, you could provide a block listing the most popular authors, or taxonomy
terms, linking to searches for those, to provide some kind of landing page.
Please note that, due to limitations in Views, this display mode is shown for
views of all base tables, even though it only works for views based on Search
API indexes. For views of other base tables, this will just print an error
message.
The display will also always ignore the view's "Style" setting, selected fields
and sorts, etc.
To use the display, specify the base path of the search you want to link to
(this enables you to also link to searches that aren't based on Views) and the
facet field to use (any indexed field can be used here, there needn't be a facet
defined for it). You'll then have the block available in the blocks
administration and can enable and move it at leisure.
Note, however, that the facet in question has to be enabled for the search page
linked to for the filter to have an effect.
Since the block will trigger a search on pages where it is set to appear, you
can also enable additional „normal“ facet blocks for that search, via the
„Facets“ tab for the index. They will automatically also point to the same
search that you specified for the display. The Search ID of the „Facets blocks“
display can easily be recognized by the "-facet_block" suffix.
If you want to use only the normal facets and not display anything at all in
the Views block, just activate the display's „Hide block“ option.
Note: If you want to display the block not only on a few pages, you should in
any case take care that it isn't displayed on the search page, since that might
confuse users.
FAQ: Why „*Indexed* Node“?
--------------------------
The group name used for the search result itself (in fields, filters, etc.) is
prefixed with „Indexed“ in order to be distinguishable from fields on referenced
nodes (or other entities). The data displayed normally still comes from the
entity, not from the search index.

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;
}
}

View File

@@ -0,0 +1,30 @@
name = Search views
description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments.
dependencies[] = search_api
dependencies[] = views
core = 7.x
package = Search
; Views handlers
files[] = includes/display_facet_block.inc
files[] = includes/handler_argument.inc
files[] = includes/handler_argument_fulltext.inc
files[] = includes/handler_argument_more_like_this.inc
files[] = includes/handler_argument_text.inc
files[] = includes/handler_filter.inc
files[] = includes/handler_filter_boolean.inc
files[] = includes/handler_filter_date.inc
files[] = includes/handler_filter_fulltext.inc
files[] = includes/handler_filter_language.inc
files[] = includes/handler_filter_options.inc
files[] = includes/handler_filter_text.inc
files[] = includes/handler_sort.inc
files[] = includes/query.inc
; Information added by drupal.org packaging script on 2013-01-09
version = "7.x-1.4"
core = "7.x"
project = "search_api"
datestamp = "1357726719"

View File

@@ -0,0 +1,97 @@
<?php
/**
* @file
* Install, update and uninstall functions for the search_api_views module.
*/
/**
* Updates all Search API views to use the new, specification-compliant identifiers.
*/
function search_api_views_update_7101() {
$tables = views_fetch_data();
// Contains arrays with real fields mapped to field IDs for each table.
$table_fields = array();
foreach ($tables as $key => $table) {
if (substr($key, 0, 17) != 'search_api_index_') {
continue;
}
foreach ($table as $field => $info) {
if (isset($info['real field']) && $field != $info['real field']) {
$table_fields[$key][$info['real field']] = $field;
}
}
}
if (!$table_fields) {
return;
}
foreach (views_get_all_views() as $name => $view) {
if (empty($view->base_table) || empty($table_fields[$view->base_table])) {
continue;
}
$change = FALSE;
$fields = $table_fields[$view->base_table];
$change |= _search_api_views_update_7101_helper($view->base_field, $fields);
if (!empty($view->display)) {
foreach ($view->display as $key => &$display) {
$options = &$display->display_options;
if (isset($options['style_options']['grouping'])) {
$change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields);
}
if (isset($options['style_options']['columns'])) {
$change |= _search_api_views_update_7101_helper($options['style_options']['columns'], $fields);
}
if (isset($options['style_options']['info'])) {
$change |= _search_api_views_update_7101_helper($options['style_options']['info'], $fields);
}
if (isset($options['arguments'])) {
$change |= _search_api_views_update_7101_helper($options['arguments'], $fields);
}
if (isset($options['fields'])) {
$change |= _search_api_views_update_7101_helper($options['fields'], $fields);
}
if (isset($options['filters'])) {
$change |= _search_api_views_update_7101_helper($options['filters'], $fields);
}
if (isset($options['sorts'])) {
$change |= _search_api_views_update_7101_helper($options['sorts'], $fields);
}
}
}
if ($change) {
$view->save();
}
}
}
/**
* Helper function for replacing field identifiers.
*
* @return
* TRUE iff the identifier was changed.
*/
function _search_api_views_update_7101_helper(&$field, array $fields) {
if (is_array($field)) {
$change = FALSE;
$new_field = array();
foreach ($field as $k => $v) {
$new_k = $k;
$change |= _search_api_views_update_7101_helper($new_k, $fields);
$change |= _search_api_views_update_7101_helper($v, $fields);
$new_field[$new_k] = $v;
}
$field = $new_field;
return $change;
}
if (isset($fields[$field])) {
$field = $fields[$field];
return TRUE;
}
return FALSE;
}
/**
* Delete the now unnecessary "search_api_views_max_fields_depth" variable.
*/
function search_api_views_update_7102() {
variable_del('search_api_views_max_fields_depth');
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* Implements hook_views_api().
*/
function search_api_views_views_api() {
return array(
'api' => '3.0-alpha1',
);
}
/**
* Implements hook_search_api_index_insert().
*/
function search_api_views_search_api_index_insert(SearchApiIndex $index) {
// Make the new index available for views.
views_invalidate_cache();
}
/**
* Implements hook_search_api_index_update().
*/
function search_api_views_search_api_index_update(SearchApiIndex $index) {
if (!$index->enabled && $index->original->enabled) {
_search_api_views_index_unavailable($index);
}
}
/**
* Implements hook_search_api_index_delete().
*/
function search_api_views_search_api_index_delete(SearchApiIndex $index) {
_search_api_views_index_unavailable($index);
}
/**
* Function for reacting to a disabled or deleted search index.
*/
function _search_api_views_index_unavailable(SearchApiIndex $index) {
$names = array();
$table = 'search_api_index_' . $index->machine_name;
foreach (views_get_all_views() as $name => $view) {
if (empty($view->disabled) && $view->base_table == $table) {
$names[] = $name;
// @todo: if ($index_deleted) $view->delete()?
}
}
if ($names) {
views_invalidate_cache();
drupal_set_message(t('The following views were using the index %name: @views. You should disable or delete them.', array('%name' => $index->name, '@views' => implode(', ', $names))), 'warning');
}
}

View File

@@ -0,0 +1,196 @@
<?php
/**
* Implements hook_views_data().
*/
function search_api_views_views_data() {
try {
$data = array();
$entity_types = entity_get_info();
foreach (search_api_index_load_multiple(FALSE) as $index) {
// Fill in base data.
$key = 'search_api_index_' . $index->machine_name;
$table = &$data[$key];
$type_info = search_api_get_item_type_info($index->item_type);
$table['table']['group'] = t('Indexed @entity_type', array('@entity_type' => $type_info['name']));
$table['table']['base'] = array(
'field' => 'search_api_id',
'index' => $index->machine_name,
'title' => $index->name,
'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)),
'query class' => 'search_api_views_query',
);
if (isset($entity_types[$index->item_type])) {
$table['table'] += array(
'entity type' => $index->item_type,
'skip entity load' => TRUE,
);
}
$wrapper = $index->entityWrapper(NULL, TRUE);
// Add field handlers and relationships provided by the Entity API.
foreach ($wrapper as $key => $property) {
$info = $property->info();
if ($info) {
entity_views_field_definition($key, $info, $table);
}
}
// Add handlers for all indexed fields.
foreach ($index->getFields() as $key => $field) {
$tmp = $wrapper;
$group = '';
$name = '';
$parts = explode(':', $key);
foreach ($parts as $i => $part) {
if (!isset($tmp->$part)) {
continue 2;
}
$tmp = $tmp->$part;
$info = $tmp->info();
$group = ($group ? $group . ' » ' . $name : ($name ? $name : ''));
$name = $info['label'];
if ($i < count($parts) - 1) {
// Unwrap lists.
$level = search_api_list_nesting_level($info['type']);
for ($j = 0; $j < $level; ++$j) {
$tmp = $tmp[0];
}
}
}
$id = _entity_views_field_identifier($key, $table);
if ($group) {
// @todo Entity type label instead of $group?
$table[$id]['group'] = $group;
$name = t('@field (indexed)', array('@field' => $name));
}
$table[$id]['title'] = $name;
$table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description'];
$table[$id]['type'] = $field['type'];
if ($id != $key) {
$table[$id]['real field'] = $key;
}
_search_api_views_add_handlers($key, $field, $tmp, $table);
}
// Special handlers
$table['search_api_language']['filter']['handler'] = 'SearchApiViewsHandlerFilterLanguage';
$table['search_api_id']['title'] = t('Entity ID');
$table['search_api_id']['help'] = t("The entity's ID.");
$table['search_api_id']['sort']['handler'] = 'SearchApiViewsHandlerSort';
$table['search_api_relevance']['group'] = t('Search');
$table['search_api_relevance']['title'] = t('Relevance');
$table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query.');
$table['search_api_relevance']['field']['type'] = 'decimal';
$table['search_api_relevance']['field']['handler'] = 'entity_views_handler_field_numeric';
$table['search_api_relevance']['field']['click sortable'] = TRUE;
$table['search_api_relevance']['sort']['handler'] = 'SearchApiViewsHandlerSort';
$table['search_api_excerpt']['group'] = t('Search');
$table['search_api_excerpt']['title'] = t('Excerpt');
$table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms.');
$table['search_api_excerpt']['field']['type'] = 'text';
$table['search_api_excerpt']['field']['handler'] = 'entity_views_handler_field_text';
$table['search_api_views_fulltext']['group'] = t('Search');
$table['search_api_views_fulltext']['title'] = t('Fulltext search');
$table['search_api_views_fulltext']['help'] = t('Search several or all fulltext fields at once.');
$table['search_api_views_fulltext']['filter']['handler'] = 'SearchApiViewsHandlerFilterFulltext';
$table['search_api_views_fulltext']['argument']['handler'] = 'SearchApiViewsHandlerArgumentFulltext';
$table['search_api_views_more_like_this']['group'] = t('Search');
$table['search_api_views_more_like_this']['title'] = t('More like this');
$table['search_api_views_more_like_this']['help'] = t('Find similar content.');
$table['search_api_views_more_like_this']['argument']['handler'] = 'SearchApiViewsHandlerArgumentMoreLikeThis';
}
return $data;
}
catch (Exception $e) {
watchdog_exception('search_api_views', $e);
}
}
/**
* Helper function that returns an array of handler definitions to add to a
* views field definition.
*/
function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $wrapper, array &$table) {
$type = $field['type'];
$inner_type = search_api_extract_inner_type($type);
if (strpos($id, ':')) {
entity_views_field_definition($id, $wrapper->info(), $table);
}
$id = _entity_views_field_identifier($id, $table);
$table += array($id => array());
if ($inner_type == 'text') {
$table[$id] += array(
'argument' => array(
'handler' => 'SearchApiViewsHandlerArgumentText',
),
'filter' => array(
'handler' => 'SearchApiViewsHandlerFilterText',
),
);
return;
}
if ($options = $wrapper->optionsList('view')) {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
$table[$id]['filter']['options'] = $options;
}
elseif ($inner_type == 'boolean') {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterBoolean';
}
elseif ($inner_type == 'date') {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
}
else {
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
}
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument';
// We can only sort according to single-valued fields.
if ($type == $inner_type) {
$table[$id]['sort']['handler'] = 'SearchApiViewsHandlerSort';
if (isset($table[$id]['field'])) {
$table[$id]['field']['click sortable'] = TRUE;
}
}
}
/**
* Implements hook_views_plugins().
*/
function search_api_views_views_plugins() {
$ret = array(
'query' => array(
'search_api_views_query' => array(
'title' => t('Search API Query'),
'help' => t('Query will be generated and run using the Search API.'),
'handler' => 'SearchApiViewsQuery'
),
),
);
if (module_exists('search_api_facetapi')) {
$ret['display']['search_api_views_facets_block'] = array(
'title' => t('Facets block'),
'help' => t('Display facets for this search as a block anywhere on the site.'),
'handler' => 'SearchApiViewsFacetsBlockDisplay',
'uses hook block' => TRUE,
'use ajax' => FALSE,
'use pager' => FALSE,
'use more' => TRUE,
'accept attachments' => TRUE,
'admin' => t('Facets block'),
);
}
return $ret;
}