merged search_api submodule
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
Search facets
|
||||
-------------
|
||||
|
||||
This module allows you to create facetted searches for any search executed via
|
||||
the Search API, no matter if executed by a search page, a view or any other
|
||||
module. The only thing you'll need is a search service class that supports the
|
||||
"search_api_facets" feature. Currently, the "Database search" and "Solr search"
|
||||
modules supports this.
|
||||
|
||||
This module is built on the Facet API [1], which is needed for this module to
|
||||
work.
|
||||
|
||||
[1] http://drupal.org/project/facetapi
|
||||
|
||||
|
||||
Information for site builders
|
||||
-----------------------------
|
||||
|
||||
For creating a facetted search, you first need a search. Create or find some
|
||||
page displaying Search API search results, either via a search page, a view or
|
||||
by any other means. Now go to the configuration page for the index on which
|
||||
this search is executed.
|
||||
If the index lies on a server supporting facets (and if this module is enabled),
|
||||
you'll notice a "Facets" tab. Click it and it will take you to the index' facet
|
||||
configuration page. You'll see a table containing all indexed fields and options
|
||||
for enabling and configuring facets for them.
|
||||
For a detailed explanation of the available options, please refer to the Facet
|
||||
API documentation.
|
||||
|
||||
- Creating facets via the URL
|
||||
|
||||
Facets can be added to a search (for which facets are activated) by passing
|
||||
appropriate GET parameters in the URL. Assuming you have an indexed field with
|
||||
the machine name "field_price", you can filter on it in the following ways:
|
||||
|
||||
- Filter for a specific value. For finding only results that have a price of
|
||||
exactly 100, pass the following $options to url() or l():
|
||||
|
||||
$options['query']['f'][] = 'field_price:100';
|
||||
|
||||
Or manually append the following GET parameter to a URL:
|
||||
|
||||
?f[0]=field_price:100
|
||||
|
||||
- Search for values in a specified range. The following example will only return
|
||||
items that have a price greater than or equal to 100 and lower than 500.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[100 TO 500]';
|
||||
URL: ?f[0]=field_price%3A%5B100%20TO%20500%5D
|
||||
|
||||
- Search for values above a value. The next example will find results which have
|
||||
a price greater than or equal to 100. The asterisk (*) stands for "unlimited",
|
||||
meaning that there is no upper limit. Filtering for values lower than a
|
||||
certain value works equivalently.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[100 TO *]';
|
||||
URL: ?f[0]=field_price%3A%5B100%20TO%20%2A%5D
|
||||
|
||||
- Search for missing values. This example will filter out all items which have
|
||||
any value at all in the price field, and will therefore only list items on
|
||||
which this field was omitted. (This naturally only makes sense for fields
|
||||
that aren't required.)
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:!';
|
||||
URL: ?f[0]=field_price%3A%21
|
||||
|
||||
- Search for present values. The following example will only return items which
|
||||
have the price field set (regardless of the actual value). You can see that it
|
||||
is actually just a range filter with unlimited lower and upper bound.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[* TO *]';
|
||||
URL: ?f[0]=field_price%3A%5B%2A%20TO%20%2A%5D
|
||||
|
||||
Note: When filtering a field whose machine name contains a colon (e.g.,
|
||||
"author:roles"), you'll have to additionally URL-encode the field name in these
|
||||
filter values:
|
||||
Code: $options['query']['f'][] = rawurlencode('author:roles') . ':100';
|
||||
URL: ?f[0]=author%253Aroles%3A100
|
||||
|
||||
- Issues
|
||||
|
||||
If you find any bugs or shortcomings while using this module, please file an
|
||||
issue in the project's issue queue [1], using the "Facets" component.
|
||||
|
||||
[1] http://drupal.org/project/issues/search_api
|
||||
|
||||
|
||||
Information for developers
|
||||
--------------------------
|
||||
|
||||
- Features
|
||||
|
||||
If you are the developer of a SearchApiServiceInterface implementation and want
|
||||
to support facets with your service class, too, you'll have to support the
|
||||
"search_api_facets" feature. You can find details about the necessary additions
|
||||
to your class in the example_servive.php file. In short, you'll just, when
|
||||
executing a query, have to return facet terms and counts according to the
|
||||
query's "search_api_facets" option, if present.
|
||||
In order for the module to be able to tell that your server supports facets,
|
||||
you will also have to change your service's supportsFeature() method to
|
||||
something like the following:
|
||||
public function supportsFeature($feature) {
|
||||
return $feature == 'search_api_facets';
|
||||
}
|
||||
|
||||
There is also a second feature defined by this module, namely
|
||||
"search_api_facets_operator_or", for supporting "OR" facets. The requirements
|
||||
for this feature are also explained in the example_servive.php file.
|
||||
|
||||
- Query option
|
||||
|
||||
The facets created follow the "search_api_base_path" option on the search query.
|
||||
If set, this path will be used as the base path from which facet links will be
|
||||
created. This can be used to show facets on pages without searches – e.g., as a
|
||||
landing page.
|
||||
|
||||
- Hidden variable
|
||||
|
||||
The module uses one hidden variable, "search_api_facets_search_ids", to keep
|
||||
track of the search IDs of searches executed for a given index. It is only
|
||||
updated when a facet is displayed for the respective search, so isn't really a
|
||||
reliable measure for this.
|
||||
In any case, if you e.g. did some test searches and now don't want them to show
|
||||
up in the block configuration forever after, just clear the variable:
|
||||
variable_del("search_api_facets_search_ids")
|
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Example implementation for a service class which supports facets.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example class explaining how facets can be supported by a service class.
|
||||
*
|
||||
* This class defines the "search_api_facets" and
|
||||
* "search_api_facets_operator_or" features. Read the method documentation and
|
||||
* inline comments in search() to learn how they can be supported by a service
|
||||
* class.
|
||||
*/
|
||||
abstract class SearchApiFacetapiExampleService extends SearchApiAbstractService {
|
||||
|
||||
/**
|
||||
* Determines whether this service class implementation supports a given
|
||||
* feature. Features are optional extensions to Search API functionality and
|
||||
* usually defined and used by third-party modules.
|
||||
*
|
||||
* If the service class supports facets, it should return TRUE if called with
|
||||
* the feature name "search_api_facets". If it also supports "OR" facets, it
|
||||
* should also return TRUE if called with "search_api_facets_operator_or".
|
||||
*
|
||||
* @param string $feature
|
||||
* The name of the optional feature.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
$supported = array(
|
||||
'search_api_facets' => TRUE,
|
||||
'search_api_facets_operator_or' => TRUE,
|
||||
);
|
||||
return isset($supported[$feature]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a search on the server represented by this object.
|
||||
*
|
||||
* If the service class supports facets, it should check for an additional
|
||||
* option on the query object:
|
||||
* - search_api_facets: An array of facets to return along with the results
|
||||
* for this query. The array is keyed by an arbitrary string which should
|
||||
* serve as the facet's unique identifier for this search. The values are
|
||||
* arrays with the following keys:
|
||||
* - field: The field to construct facets for.
|
||||
* - limit: The maximum number of facet terms to return. 0 or an empty
|
||||
* value means no limit.
|
||||
* - min_count: The minimum number of results a facet value has to have in
|
||||
* order to be returned.
|
||||
* - missing: If TRUE, a facet for all items with no value for this field
|
||||
* should be returned (if it conforms to limit and min_count).
|
||||
* - operator: (optional) If the service supports "OR" facets and this key
|
||||
* contains the string "or", the returned facets should be "OR" facets. If
|
||||
* the server doesn't support "OR" facets, this key can be ignored.
|
||||
*
|
||||
* The basic principle of facets is explained quite well in the
|
||||
* @link http://en.wikipedia.org/wiki/Faceted_search Wikipedia article on
|
||||
* "Faceted search" @endlink. Basically, you should return for each field
|
||||
* filter values which would yield some results when used with the search.
|
||||
* E.g., if you return for a field $field the term $term with $count results,
|
||||
* the given $query along with
|
||||
* $query->condition($field, $term)
|
||||
* should yield exactly (or about) $count results.
|
||||
*
|
||||
* For "OR" facets, all existing filters on the facetted field should be
|
||||
* ignored for computing the facets.
|
||||
*
|
||||
* @param $query
|
||||
* The SearchApiQueryInterface object to execute.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as required by
|
||||
* SearchApiQueryInterface::execute().
|
||||
* In addition, if the "search_api_facets" option is present on the query,
|
||||
* the results should contain an array of facets in the "search_api_facets"
|
||||
* key, as specified by the option. The facets array should be keyed by the
|
||||
* facets' unique identifiers, and contain a numeric array of facet terms,
|
||||
* sorted descending by result count. A term is represented by an array with
|
||||
* the following keys:
|
||||
* - count: Number of results for this term.
|
||||
* - filter: The filter to apply when selecting this facet term. A filter is
|
||||
* a string of one of the following forms:
|
||||
* - "VALUE": Filter by the literal value VALUE (always include the
|
||||
* quotes, not only for strings).
|
||||
* - [VALUE1 VALUE2]: Filter for a value between VALUE1 and VALUE2. Use
|
||||
* parantheses for excluding the border values and square brackets for
|
||||
* including them. An asterisk (*) can be used as a wildcard. E.g.,
|
||||
* (* 0) or [* 0) would be a filter for all negative values.
|
||||
* - !: Filter for items without a value for this field (i.e., the
|
||||
* "missing" facet).
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error prevented the search from completing.
|
||||
*/
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
// We assume here that we have an AI search which understands English
|
||||
// commands.
|
||||
|
||||
// First, create the normal search query, without facets.
|
||||
$search = new SuperCoolAiSearch($query->getIndex());
|
||||
$search->cmd('create basic search for the following query', $query);
|
||||
$ret = $search->cmd('return search results in Search API format');
|
||||
|
||||
// Then, let's see if we should return any facets.
|
||||
if ($facets = $query->getOption('search_api_facets')) {
|
||||
// For the facets, we need all results, not only those in the specified
|
||||
// range.
|
||||
$results = $search->cmd('return unlimited search results as a set');
|
||||
foreach ($facets as $id => $facet) {
|
||||
$field = $facet['field'];
|
||||
$limit = empty($facet['limit']) ? 'all' : $facet['limit'];
|
||||
$min_count = $facet['min_count'];
|
||||
$missing = $facet['missing'];
|
||||
$or = isset($facet['operator']) && $facet['operator'] == 'or';
|
||||
|
||||
// If this is an "OR" facet, existing filters on the field should be
|
||||
// ignored for computing the facets.
|
||||
// You can ignore this if your service class doesn't support the
|
||||
// "search_api_facets_operator_or" feature.
|
||||
if ($or) {
|
||||
// We have to execute another query (in the case of this hypothetical
|
||||
// search backend, at least) to get the right result set to facet.
|
||||
$tmp_search = new SuperCoolAiSearch($query->getIndex());
|
||||
$tmp_search->cmd('create basic search for the following query', $query);
|
||||
$tmp_search->cmd("remove all conditions for field $field");
|
||||
$tmp_results = $tmp_search->cmd('return unlimited search results as a set');
|
||||
}
|
||||
else {
|
||||
// Otherwise, we can just use the normal results.
|
||||
$tmp_results = $results;
|
||||
}
|
||||
|
||||
$filters = array();
|
||||
if ($search->cmd("$field is a date or numeric field")) {
|
||||
// For date, integer or float fields, range facets are more useful.
|
||||
$ranges = $search->cmd("list $limit ranges of field $field in the following set", $tmp_results);
|
||||
foreach ($ranges as $range) {
|
||||
if ($range->getCount() >= $min_count) {
|
||||
// Get the lower and upper bound of the range. * means unlimited.
|
||||
$lower = $range->getLowerBound();
|
||||
$lower = ($lower == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $lower;
|
||||
$upper = $range->getUpperBound();
|
||||
$upper = ($upper == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $upper;
|
||||
// Then, see whether the bounds are included in the range. These
|
||||
// can be specified independently for the lower and upper bound.
|
||||
// Parentheses are used for exclusive bounds, square brackets are
|
||||
// used for inclusive bounds.
|
||||
$lowChar = $range->isLowerBoundInclusive() ? '[' : '(';
|
||||
$upChar = $range->isUpperBoundInclusive() ? ']' : ')';
|
||||
// Create the filter, which separates the bounds with a single
|
||||
// space.
|
||||
$filter = "$lowChar$lower $upper$upChar";
|
||||
$filters[$filter] = $range->getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Otherwise, we use normal single-valued facets.
|
||||
$terms = $search->cmd("list $limit values of field $field in the following set", $tmp_results);
|
||||
foreach ($terms as $term) {
|
||||
if ($term->getCount() >= $min_count) {
|
||||
// For single-valued terms, we just need to wrap them in quotes.
|
||||
$filter = '"' . $term->getValue() . '"';
|
||||
$filters[$filter] = $term->getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we should also return a "missing" facet, compute that as the
|
||||
// number of results without a value for the facet field.
|
||||
if ($missing) {
|
||||
$count = $search->cmd("return number of results without field $field in the following set", $tmp_results);
|
||||
if ($count >= $min_count) {
|
||||
$filters['!'] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the facets descending by result count.
|
||||
arsort($filters);
|
||||
|
||||
// With the "missing" facet, we might have too many facet terms (unless
|
||||
// $limit was empty and is therefore now set to "all"). If this is the
|
||||
// case, remove those with the lowest number of results.
|
||||
while (is_numeric($limit) && count($filters) > $limit) {
|
||||
array_pop($filters);
|
||||
}
|
||||
|
||||
// Now add the facet terms to the return value, as specified in the doc
|
||||
// comment for this method.
|
||||
foreach ($filters as $filter => $count) {
|
||||
$ret['search_api_facets'][$id][] = array(
|
||||
'count' => $count,
|
||||
'filter' => $filter,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the results, which now also includes the facet information.
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Classes used by the Facet API module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Facet API adapter for the Search API module.
|
||||
*/
|
||||
class SearchApiFacetapiAdapter extends FacetapiAdapter {
|
||||
|
||||
/**
|
||||
* Cached value for the current search for this searcher, if any.
|
||||
*
|
||||
* @see getCurrentSearch()
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $current_search;
|
||||
|
||||
/**
|
||||
* The active facet fields for the current search.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = array();
|
||||
|
||||
/**
|
||||
* Returns the path to the admin settings for a given realm.
|
||||
*
|
||||
* @param $realm_name
|
||||
* The name of the realm.
|
||||
*
|
||||
* @return
|
||||
* The path to the admin settings.
|
||||
*/
|
||||
public function getPath($realm_name) {
|
||||
$base_path = 'admin/config/search/search_api';
|
||||
$index_id = $this->info['instance'];
|
||||
return $base_path . '/index/' . $index_id . '/facets/' . $realm_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides FacetapiAdapter::getSearchPath().
|
||||
*/
|
||||
public function getSearchPath() {
|
||||
$search = $this->getCurrentSearch();
|
||||
if ($search && $search[0]->getOption('search_api_base_path')) {
|
||||
return $search[0]->getOption('search_api_base_path');
|
||||
}
|
||||
return $_GET['q'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the backend to initialize its query object before adding the facet filters.
|
||||
*
|
||||
* @param mixed $query
|
||||
* The backend's native object.
|
||||
*/
|
||||
public function initActiveFilters($query) {
|
||||
$search_id = $query->getOption('search id');
|
||||
$index_id = $this->info['instance'];
|
||||
$facets = facetapi_get_enabled_facets($this->info['name']);
|
||||
$this->fields = array();
|
||||
|
||||
// We statically store the current search per facet so that we can correctly
|
||||
// assign it when building the facets. See the build() method in the query
|
||||
// type plugin classes.
|
||||
$active = &drupal_static('search_api_facetapi_active_facets', array());
|
||||
foreach ($facets as $facet) {
|
||||
$options = $this->getFacet($facet)->getSettings()->settings;
|
||||
// The 'default_true' option is a choice between "show on all but the
|
||||
// selected searches" (TRUE) and "show for only the selected searches".
|
||||
$default_true = isset($options['default_true']) ? $options['default_true'] : TRUE;
|
||||
// The 'facet_search_ids' option is the list of selected searches that
|
||||
// will either be excluded or for which the facet will exclusively be
|
||||
// displayed.
|
||||
$facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array();
|
||||
|
||||
if (array_search($search_id, $facet_search_ids) === FALSE) {
|
||||
$search_ids = variable_get('search_api_facets_search_ids', array());
|
||||
if (empty($search_ids[$index_id][$search_id])) {
|
||||
// Remember this search ID.
|
||||
$search_ids[$index_id][$search_id] = $search_id;
|
||||
variable_set('search_api_facets_search_ids', $search_ids);
|
||||
}
|
||||
if (!$default_true) {
|
||||
continue; // We are only to show facets for explicitly named search ids.
|
||||
}
|
||||
}
|
||||
elseif ($default_true) {
|
||||
continue; // The 'facet_search_ids' in the settings are to be excluded.
|
||||
}
|
||||
$active[$facet['name']] = $search_id;
|
||||
$this->fields[$facet['name']] = array(
|
||||
'field' => $facet['field'],
|
||||
'limit' => $options['hard_limit'],
|
||||
'operator' => $options['operator'],
|
||||
'min_count' => $options['facet_mincount'],
|
||||
'missing' => $options['facet_missing'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given facet to the query.
|
||||
*/
|
||||
public function addFacet(array $facet, SearchApiQueryInterface $query) {
|
||||
if (isset($this->fields[$facet['name']])) {
|
||||
$options = &$query->getOptions();
|
||||
$facet_info = $this->fields[$facet['name']];
|
||||
if (!empty($facet['query_options'])) {
|
||||
// Let facet-specific query options override the set options.
|
||||
$facet_info = $facet['query_options'] + $facet_info;
|
||||
}
|
||||
$options['search_api_facets'][$facet['name']] = $facet_info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean flagging whether $this->_searcher executed a search.
|
||||
*/
|
||||
public function searchExecuted() {
|
||||
return (bool) $this->getCurrentSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for getting a current search for this searcher.
|
||||
*
|
||||
* @return array
|
||||
* The first matching current search, in the form specified by
|
||||
* search_api_current_search(). Or NULL, if no match was found.
|
||||
*/
|
||||
public function getCurrentSearch() {
|
||||
// Even if this fails once, there might be a search query later in the page
|
||||
// request. We therefore don't store anything in $this->current_search in
|
||||
// case of failure, but just try again if the method is called again.
|
||||
if (!isset($this->current_search)) {
|
||||
$index_id = $this->info['instance'];
|
||||
// There is currently no way to configure the "current search" block to
|
||||
// show on a per-searcher basis as we do with the facets. Therefore we
|
||||
// cannot match it up to the correct "current search".
|
||||
// I suspect that http://drupal.org/node/593658 would help.
|
||||
// For now, just taking the first current search for this index. :-/
|
||||
foreach (search_api_current_search() as $search) {
|
||||
list($query) = $search;
|
||||
if ($query->getIndex()->machine_name == $index_id) {
|
||||
$this->current_search = $search;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->current_search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean flagging whether facets in a realm shoud be displayed.
|
||||
*
|
||||
* Useful, for example, for suppressing sidebar blocks in some cases.
|
||||
*
|
||||
* @return
|
||||
* A boolean flagging whether to display a given realm.
|
||||
*/
|
||||
public function suppressOutput($realm_name) {
|
||||
// Not sure under what circumstances the output will need to be suppressed?
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search keys.
|
||||
*/
|
||||
public function getSearchKeys() {
|
||||
$search = $this->getCurrentSearch();
|
||||
$keys = $search[0]->getOriginalKeys();
|
||||
if (is_array($keys)) {
|
||||
// This will happen nearly never when displaying the search keys to the
|
||||
// user, so go with a simple work-around.
|
||||
// If someone complains, we can easily add a method for printing them
|
||||
// properly.
|
||||
$keys = '[' . t('complex query') . ']';
|
||||
}
|
||||
drupal_alter('search_api_facetapi_keys', $keys, $search[0]);
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of total results found for the current search.
|
||||
*/
|
||||
public function getResultCount() {
|
||||
$search = $this->getCurrentSearch();
|
||||
// Each search is an array with the query as the first element and the results
|
||||
// array as the second.
|
||||
if (isset($search[1])) {
|
||||
return $search[1]['result count'];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for backend specific overrides to the settings form.
|
||||
*/
|
||||
public function settingsForm(&$form, &$form_state) {
|
||||
$facet = $form['#facetapi']['facet'];
|
||||
$facet_settings = $this->getFacet($facet)->getSettings();
|
||||
$options = $facet_settings->settings;
|
||||
$search_ids = variable_get('search_api_facets_search_ids', array());
|
||||
$search_ids = isset($search_ids[$this->info['instance']]) ? $search_ids[$this->info['instance']] : array();
|
||||
if (count($search_ids) > 1) {
|
||||
$form['global']['default_true'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Display for searches'),
|
||||
'#prefix' => '<div class="facetapi-global-setting">',
|
||||
'#options' => array(
|
||||
TRUE => t('For all except the selected'),
|
||||
FALSE => t('Only for the selected'),
|
||||
),
|
||||
'#default_value' => isset($options['default_true']) ? $options['default_true'] : TRUE,
|
||||
);
|
||||
$form['global']['facet_search_ids'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Search IDs'),
|
||||
'#suffix' => '</div>',
|
||||
'#options' => $search_ids,
|
||||
'#size' => min(4, count($search_ids)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['global']['default_true'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => TRUE,
|
||||
);
|
||||
$form['global']['facet_search_ids'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
// Add a granularity option to date query types.
|
||||
if (isset($facet['query type']) && $facet['query type'] == 'date') {
|
||||
$granularity_options = array(
|
||||
FACETAPI_DATE_YEAR => t('Years'),
|
||||
FACETAPI_DATE_MONTH => t('Months'),
|
||||
FACETAPI_DATE_DAY => t('Days'),
|
||||
FACETAPI_DATE_HOUR => t('Hours'),
|
||||
FACETAPI_DATE_MINUTE => t('Minutes'),
|
||||
FACETAPI_DATE_SECOND => t('Seconds'),
|
||||
);
|
||||
|
||||
$form['global']['date_granularity'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Granularity'),
|
||||
'#description' => t('Determine the maximum drill-down level'),
|
||||
'#prefix' => '<div class="facetapi-global-setting">',
|
||||
'#suffix' => '</div>',
|
||||
'#options' => $granularity_options,
|
||||
'#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE,
|
||||
);
|
||||
}
|
||||
|
||||
// Add an "Exclude" option for terms.
|
||||
if(!empty($facet['query types']) && in_array('term', $facet['query types'])) {
|
||||
$form['global']['operator']['#weight'] = -2;
|
||||
unset($form['global']['operator']['#suffix']);
|
||||
$form['global']['exclude'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Exclude'),
|
||||
'#description' => t('Make the search exclude selected facets, instead of restricting it to them.'),
|
||||
'#suffix' => '</div>',
|
||||
'#weight' => -1,
|
||||
'#default_value' => !empty($options['exclude']),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Date query type plugin for the Search API adapter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin for "date" query types.
|
||||
*/
|
||||
class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQueryTypeInterface {
|
||||
|
||||
/**
|
||||
* Loads the include file containing the date API functions.
|
||||
*/
|
||||
public function __construct(FacetapiAdapter $adapter, array $facet) {
|
||||
module_load_include('date.inc', 'facetapi');
|
||||
parent::__construct($adapter, $facet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query type associated with the plugin.
|
||||
*
|
||||
* @return string
|
||||
* The query type.
|
||||
*/
|
||||
static public function getType() {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the filter to the query object.
|
||||
*
|
||||
* @param $query
|
||||
* An object containing the query in the backend's native API.
|
||||
*/
|
||||
public function execute($query) {
|
||||
// Return terms for this facet.
|
||||
$this->adapter->addFacet($this->facet, $query);
|
||||
|
||||
$settings = $this->adapter->getFacet($this->facet)->getSettings()->settings;
|
||||
|
||||
// First check if the facet is enabled for this search.
|
||||
$default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
|
||||
$facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array();
|
||||
if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) {
|
||||
// Facet is not enabled for this search ID.
|
||||
return;
|
||||
}
|
||||
|
||||
// Change limit to "unlimited" (-1).
|
||||
$options = &$query->getOptions();
|
||||
if (!empty($options['search_api_facets'][$this->facet['name']])) {
|
||||
$options['search_api_facets'][$this->facet['name']]['limit'] = -1;
|
||||
}
|
||||
|
||||
if ($active = $this->adapter->getActiveItems($this->facet)) {
|
||||
$item = end($active);
|
||||
$field = $this->facet['field'];
|
||||
$regex = str_replace(array('^', '$'), '', FACETAPI_REGEX_DATE);
|
||||
$filter = preg_replace_callback($regex, array($this, 'replaceDateString'), $item['value']);
|
||||
$this->addFacetFilter($query, $field, $filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement callback for replacing ISO dates with timestamps.
|
||||
*/
|
||||
public function replaceDateString($matches) {
|
||||
return strtotime($matches[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the facet's build array.
|
||||
*
|
||||
* @return array
|
||||
* The initialized render array.
|
||||
*/
|
||||
public function build() {
|
||||
$facet = $this->adapter->getFacet($this->facet);
|
||||
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
|
||||
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
|
||||
return array();
|
||||
}
|
||||
$search_id = $search_ids[$facet['name']];
|
||||
$build = array();
|
||||
$search = search_api_current_search($search_id);
|
||||
$results = $search[1];
|
||||
if (!$results['result count']) {
|
||||
return array();
|
||||
}
|
||||
// Gets total number of documents matched in search.
|
||||
$total = $results['result count'];
|
||||
|
||||
// Most of the code below is copied from search_facetapi's implementation of
|
||||
// this method.
|
||||
|
||||
// Executes query, iterates over results.
|
||||
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
|
||||
$values = $results['search_api_facets'][$this->facet['name']];
|
||||
foreach ($values as $value) {
|
||||
if ($value['count']) {
|
||||
$filter = $value['filter'];
|
||||
// We only process single values further. The "missing" filter and
|
||||
// range filters will be passed on unchanged.
|
||||
if ($filter == '!') {
|
||||
$build[$filter]['#count'] = $value['count'];
|
||||
}
|
||||
elseif ($filter[0] == '"') {
|
||||
$filter = substr($value['filter'], 1, -1);
|
||||
if ($filter) {
|
||||
$raw_values[$filter] = $value['count'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$filter = substr($value['filter'], 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$lower = facetapi_isodate(substr($filter, 0, $pos), FACETAPI_DATE_DAY);
|
||||
$upper = facetapi_isodate(substr($filter, $pos + 1), FACETAPI_DATE_DAY);
|
||||
$filter = '[' . $lower . ' TO ' . $upper . ']';
|
||||
}
|
||||
$build[$filter]['#count'] = $value['count'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the finest level of detail we're allowed to drill down to.
|
||||
$settings = $facet->getSettings()->settings;
|
||||
$granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;
|
||||
|
||||
// Gets active facets, starts building hierarchy.
|
||||
$parent = $gap = NULL;
|
||||
$active_items = $this->adapter->getActiveItems($this->facet);
|
||||
foreach ($active_items as $value => $item) {
|
||||
// If the item is active, the count is the result set count.
|
||||
$build[$value] = array('#count' => $total);
|
||||
|
||||
// Gets next "gap" increment.
|
||||
if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) {
|
||||
continue;
|
||||
}
|
||||
$start = substr($value, 1, $pos);
|
||||
$end = substr($value, $pos + 4, -1);
|
||||
$date_gap = facetapi_get_date_gap($start, $end);
|
||||
$gap = facetapi_get_next_date_gap($date_gap, $granularity);
|
||||
|
||||
// If there is a previous item, there is a parent, uses a reference so the
|
||||
// arrays are populated when they are updated.
|
||||
if (NULL !== $parent) {
|
||||
$build[$parent]['#item_children'][$value] = &$build[$value];
|
||||
$build[$value]['#item_parents'][$parent] = $parent;
|
||||
}
|
||||
|
||||
// Stores the last value iterated over.
|
||||
$parent = $value;
|
||||
}
|
||||
if (empty($raw_values)) {
|
||||
return $build;
|
||||
}
|
||||
ksort($raw_values);
|
||||
|
||||
// Mind the gap! Calculates gap from min and max timestamps.
|
||||
$timestamps = array_keys($raw_values);
|
||||
if (NULL === $parent) {
|
||||
if (count($raw_values) > 1) {
|
||||
$gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps));
|
||||
// Array of numbers used to determine whether the next gap is smaller than
|
||||
// the minimum gap allowed in the drilldown.
|
||||
$gap_numbers = array(
|
||||
FACETAPI_DATE_YEAR => 6,
|
||||
FACETAPI_DATE_MONTH => 5,
|
||||
FACETAPI_DATE_DAY => 4,
|
||||
FACETAPI_DATE_HOUR => 3,
|
||||
FACETAPI_DATE_MINUTE => 2,
|
||||
FACETAPI_DATE_SECOND => 1,
|
||||
);
|
||||
// Gets gap numbers for both the gap and minimum gap, checks if the gap
|
||||
// is within the limit set by the $granularity parameter.
|
||||
if ($gap_numbers[$gap] < $gap_numbers[$granularity]) {
|
||||
$gap = $granularity;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$gap = $granularity;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts all timestamps to dates in ISO 8601 format.
|
||||
$dates = array();
|
||||
foreach ($timestamps as $timestamp) {
|
||||
$dates[$timestamp] = facetapi_isodate($timestamp, $gap);
|
||||
}
|
||||
|
||||
// Treat each date as the range start and next date as the range end.
|
||||
$range_end = array();
|
||||
$previous = NULL;
|
||||
foreach (array_unique($dates) as $date) {
|
||||
if (NULL !== $previous) {
|
||||
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
|
||||
}
|
||||
$previous = $date;
|
||||
}
|
||||
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
|
||||
|
||||
// Groups dates by the range they belong to, builds the $build array
|
||||
// with the facet counts and formatted range values.
|
||||
foreach ($raw_values as $value => $count) {
|
||||
$new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']';
|
||||
if (!isset($build[$new_value])) {
|
||||
$build[$new_value] = array('#count' => $count);
|
||||
}
|
||||
// Active items already have their value set because it's the current
|
||||
// result count.
|
||||
elseif (!isset($active_items[$new_value])) {
|
||||
$build[$new_value]['#count'] += $count;
|
||||
}
|
||||
|
||||
// Adds parent information if not already set.
|
||||
if (NULL !== $parent && $parent != $new_value) {
|
||||
$build[$parent]['#item_children'][$new_value] = &$build[$new_value];
|
||||
$build[$new_value]['#item_parents'][$parent] = $parent;
|
||||
}
|
||||
}
|
||||
|
||||
return $build;
|
||||
}
|
||||
}
|
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Term query type plugin for the Apache Solr adapter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin for "term" query types.
|
||||
*/
|
||||
class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTypeInterface {
|
||||
|
||||
/**
|
||||
* Returns the query type associated with the plugin.
|
||||
*
|
||||
* @return string
|
||||
* The query type.
|
||||
*/
|
||||
static public function getType() {
|
||||
return 'term';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the filter to the query object.
|
||||
*
|
||||
* @param SearchApiQueryInterface $query
|
||||
* An object containing the query in the backend's native API.
|
||||
*/
|
||||
public function execute($query) {
|
||||
// Return terms for this facet.
|
||||
$this->adapter->addFacet($this->facet, $query);
|
||||
|
||||
$settings = $this->adapter->getFacet($this->facet)->getSettings()->settings;
|
||||
|
||||
// First check if the facet is enabled for this search.
|
||||
$default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
|
||||
$facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : array();
|
||||
if ($default_true != empty($facet_search_ids[$query->getOption('search id')])) {
|
||||
// Facet is not enabled for this search ID.
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the active facet filters.
|
||||
$active = $this->adapter->getActiveItems($this->facet);
|
||||
if (empty($active)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the facet filter, and add a tag to it so that it can be easily
|
||||
// identified down the line by services when they need to exclude facets.
|
||||
$operator = $settings['operator'];
|
||||
if ($operator == FACETAPI_OPERATOR_AND) {
|
||||
$conjunction = 'AND';
|
||||
}
|
||||
elseif ($operator == FACETAPI_OPERATOR_OR) {
|
||||
$conjunction = 'OR';
|
||||
}
|
||||
else {
|
||||
throw new SearchApiException(t('Unknown facet operator %operator.', array('%operator' => $operator)));
|
||||
}
|
||||
$tags = array('facet:' . $this->facet['field']);
|
||||
$facet_filter = $query->createFilter($conjunction, $tags);
|
||||
|
||||
foreach ($active as $filter => $filter_array) {
|
||||
$field = $this->facet['field'];
|
||||
$this->addFacetFilter($facet_filter, $field, $filter);
|
||||
}
|
||||
|
||||
// Now add the filter to the query.
|
||||
$query->filter($facet_filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for setting a facet filter on a query or query filter object.
|
||||
*/
|
||||
protected function addFacetFilter($query_filter, $field, $filter) {
|
||||
// Test if this filter should be negated.
|
||||
$settings = $this->adapter->getFacet($this->facet)->getSettings();
|
||||
$exclude = !empty($settings->settings['exclude']);
|
||||
// Integer (or other nun-string) filters might mess up some of the following
|
||||
// comparison expressions.
|
||||
$filter = (string) $filter;
|
||||
if ($filter == '!') {
|
||||
$query_filter->condition($field, NULL, $exclude ? '<>' : '=');
|
||||
}
|
||||
elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
|
||||
$lower = trim(substr($filter, 1, $pos));
|
||||
$upper = trim(substr($filter, $pos + 4, -1));
|
||||
if ($lower == '*' && $upper == '*') {
|
||||
$query_filter->condition($field, NULL, $exclude ? '=' : '<>');
|
||||
}
|
||||
elseif (!$exclude) {
|
||||
if ($lower != '*') {
|
||||
// Iff we have a range with two finite boundaries, we set two
|
||||
// conditions (larger than the lower bound and less than the upper
|
||||
// bound) and therefore have to make sure that we have an AND
|
||||
// conjunction for those.
|
||||
if ($upper != '*' && !($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
|
||||
$original_query_filter = $query_filter;
|
||||
$query_filter = new SearchApiQueryFilter('AND');
|
||||
}
|
||||
$query_filter->condition($field, $lower, '>=');
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$query_filter->condition($field, $upper, '<=');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Same as above, but with inverted logic.
|
||||
if ($lower != '*') {
|
||||
if ($upper != '*' && ($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
|
||||
$original_query_filter = $query_filter;
|
||||
$query_filter = new SearchApiQueryFilter('OR');
|
||||
}
|
||||
$query_filter->condition($field, $lower, '<');
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$query_filter->condition($field, $upper, '>');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$query_filter->condition($field, $filter, $exclude ? '<>' : '=');
|
||||
}
|
||||
if (isset($original_query_filter)) {
|
||||
$original_query_filter->filter($query_filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the facet's build array.
|
||||
*
|
||||
* @return array
|
||||
* The initialized render array.
|
||||
*/
|
||||
public function build() {
|
||||
$facet = $this->adapter->getFacet($this->facet);
|
||||
// The current search per facet is stored in a static variable (during
|
||||
// initActiveFilters) so that we can retrieve it here and get the correct
|
||||
// current search for this facet.
|
||||
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
|
||||
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
|
||||
return array();
|
||||
}
|
||||
$search_id = $search_ids[$facet['name']];
|
||||
$search = search_api_current_search($search_id);
|
||||
$build = array();
|
||||
$results = $search[1];
|
||||
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
|
||||
$values = $results['search_api_facets'][$this->facet['name']];
|
||||
foreach ($values as $value) {
|
||||
$filter = $value['filter'];
|
||||
// As Facet API isn't really suited for our native facet filter
|
||||
// representations, convert the format here. (The missing facet can
|
||||
// stay the same.)
|
||||
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) . ']';
|
||||
}
|
||||
}
|
||||
$build[$filter] = array(
|
||||
'#count' => $value['count'],
|
||||
);
|
||||
}
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Search facets module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets modules alter the search keys that are returned to FacetAPI and used
|
||||
* in the current search block and breadcrumb trail.
|
||||
*
|
||||
* @param string $keys
|
||||
* The string representing the user's current search query.
|
||||
* @param SearchApiQuery $query
|
||||
* The SearchApiQuery object for the current search.
|
||||
*/
|
||||
function hook_search_api_facetapi_keys_alter(&$keys, $query) {
|
||||
if ($keys == '[' . t('all items') . ']') {
|
||||
// Change $keys to something else, perhaps based on filters in the query
|
||||
// object.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
@@ -0,0 +1,17 @@
|
||||
name = Search facets
|
||||
description = "Integrate the Search API with the Facet API to provide facetted searches."
|
||||
dependencies[] = search_api
|
||||
dependencies[] = facetapi
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
files[] = plugins/facetapi/adapter.inc
|
||||
files[] = plugins/facetapi/query_type_term.inc
|
||||
files[] = plugins/facetapi/query_type_date.inc
|
||||
|
||||
; Information added by Drupal.org packaging script on 2013-12-25
|
||||
version = "7.x-1.11"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1387965506"
|
||||
|
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the Search facets module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function search_api_facetapi_uninstall() {
|
||||
variable_del('search_api_facets_search_ids');
|
||||
}
|
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Integrates the Search API with the Facet API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function search_api_facetapi_menu() {
|
||||
// We need to handle our own menu paths for facets because we need a facet
|
||||
// configuration page per index.
|
||||
$first = TRUE;
|
||||
foreach (facetapi_get_realm_info() as $realm_name => $realm) {
|
||||
if ($first) {
|
||||
$first = FALSE;
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets'] = array(
|
||||
'title' => 'Facets',
|
||||
'page callback' => 'search_api_facetapi_settings',
|
||||
'page arguments' => array($realm_name, 5),
|
||||
'weight' => -1,
|
||||
'access arguments' => array('administer search_api'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
|
||||
);
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
|
||||
'title' => $realm['label'],
|
||||
'type' => MENU_DEFAULT_LOCAL_TASK,
|
||||
'weight' => $realm['weight'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
|
||||
'title' => $realm['label'],
|
||||
'page callback' => 'search_api_facetapi_settings',
|
||||
'page arguments' => array($realm_name, 5),
|
||||
'access arguments' => array('administer search_api'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
|
||||
'weight' => $realm['weight'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_searcher_info().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_searcher_info() {
|
||||
$info = array();
|
||||
$indexes = search_api_index_load_multiple(FALSE);
|
||||
foreach ($indexes as $index) {
|
||||
if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) {
|
||||
$searcher_name = 'search_api@' . $index->machine_name;
|
||||
$info[$searcher_name] = array(
|
||||
'label' => t('Search service: @name', array('@name' => $index->name)),
|
||||
'adapter' => 'search_api',
|
||||
'instance' => $index->machine_name,
|
||||
'types' => array($index->item_type),
|
||||
'path' => '',
|
||||
'supports facet missing' => TRUE,
|
||||
'supports facet mincount' => TRUE,
|
||||
'include default facets' => FALSE,
|
||||
);
|
||||
if (($entity_type = $index->getEntityType()) && $entity_type !== $index->item_type) {
|
||||
$info[$searcher_name]['types'][] = $entity_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_facet_info().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
|
||||
$facet_info = array();
|
||||
if ('search_api' == $searcher_info['adapter']) {
|
||||
$index = search_api_index_load($searcher_info['instance']);
|
||||
if (!empty($index->options['fields'])) {
|
||||
$wrapper = $index->entityWrapper();
|
||||
$bundle_key = NULL;
|
||||
if ($index->getEntityType() && ($entity_info = entity_get_info($index->getEntityType())) && !empty($entity_info['bundle keys']['bundle'])) {
|
||||
$bundle_key = $entity_info['bundle keys']['bundle'];
|
||||
}
|
||||
|
||||
// Some type-specific settings. Allowing to set some additional callbacks
|
||||
// (and other settings) in the map options allows for easier overriding by
|
||||
// other modules.
|
||||
$type_settings = array(
|
||||
'taxonomy_term' => array(
|
||||
'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy',
|
||||
),
|
||||
'date' => array(
|
||||
'query type' => 'date',
|
||||
'map options' => array(
|
||||
'map callback' => 'facetapi_map_date',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Iterate through the indexed fields to set the facetapi settings for
|
||||
// each one.
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$field['key'] = $key;
|
||||
// Determine which, if any, of the field type-specific options will be
|
||||
// used for this field.
|
||||
$type = isset($field['entity_type']) ? $field['entity_type'] : $field['type'];
|
||||
$type_settings += array($type => array());
|
||||
|
||||
$facet_info[$key] = $type_settings[$type] + array(
|
||||
'label' => $field['name'],
|
||||
'description' => t('Filter by @type.', array('@type' => $field['name'])),
|
||||
'allowed operators' => array(
|
||||
FACETAPI_OPERATOR_AND => TRUE,
|
||||
FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'),
|
||||
),
|
||||
'dependency plugins' => array('role'),
|
||||
'facet missing allowed' => TRUE,
|
||||
'facet mincount allowed' => TRUE,
|
||||
'map callback' => 'search_api_facetapi_facet_map_callback',
|
||||
'map options' => array(),
|
||||
'field type' => $type,
|
||||
);
|
||||
if ($type === 'date') {
|
||||
$facet_info[$key]['description'] .= ' ' . t('(Caution: This may perform very poorly for large result sets.)');
|
||||
}
|
||||
$facet_info[$key]['map options'] += array(
|
||||
'field' => $field,
|
||||
'index id' => $index->machine_name,
|
||||
'value callback' => '_search_api_facetapi_facet_create_label',
|
||||
);
|
||||
// Find out whether this property is a Field API field.
|
||||
if (strpos($key, ':') === FALSE) {
|
||||
if (isset($wrapper->$key)) {
|
||||
$property_info = $wrapper->$key->info();
|
||||
if (!empty($property_info['field'])) {
|
||||
$facet_info[$key]['field api name'] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add bundle information, if applicable.
|
||||
if ($bundle_key) {
|
||||
if ($key === $bundle_key) {
|
||||
// Set entity type this field contains bundle information for.
|
||||
$facet_info[$key]['field api bundles'][] = $index->getEntityType();
|
||||
}
|
||||
else {
|
||||
// Add "bundle" as possible dependency plugin.
|
||||
$facet_info[$key]['dependency plugins'][] = 'bundle';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $facet_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_adapters().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_adapters() {
|
||||
return array(
|
||||
'search_api' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiAdapter',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_query_types().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_query_types() {
|
||||
return array(
|
||||
'search_api_term' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiTerm',
|
||||
'adapter' => 'search_api',
|
||||
),
|
||||
),
|
||||
'search_api_date' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiDate',
|
||||
'adapter' => 'search_api',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_query_alter().
|
||||
*
|
||||
* Adds Facet API support to the query.
|
||||
*/
|
||||
function search_api_facetapi_search_api_query_alter($query) {
|
||||
$index = $query->getIndex();
|
||||
if ($index->server()->supportsFeature('search_api_facets')) {
|
||||
// This is the main point of communication between the facet system and the
|
||||
// search back-end - it makes the query respond to active facets.
|
||||
$searcher = 'search_api@' . $index->machine_name;
|
||||
$adapter = facetapi_adapter_load($searcher);
|
||||
if ($adapter) {
|
||||
$adapter->addActiveFilters($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback for the facet settings page.
|
||||
*/
|
||||
function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
|
||||
if (!$index->enabled) {
|
||||
return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
|
||||
}
|
||||
if (!$index->server()->supportsFeature('search_api_facets')) {
|
||||
return array('#markup' => t('This index uses a server that does not support facet functionality.'));
|
||||
}
|
||||
$searcher_name = 'search_api@' . $index->machine_name;
|
||||
module_load_include('inc', 'facetapi', 'facetapi.admin');
|
||||
return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets hierarchy information for taxonomy terms.
|
||||
*
|
||||
* Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info().
|
||||
*
|
||||
* Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that
|
||||
* our special "!" value is not passed.
|
||||
*
|
||||
* @param array $values
|
||||
* An array containing the term IDs.
|
||||
*
|
||||
* @return array
|
||||
* An associative array mapping term IDs to parent IDs (where parents could be
|
||||
* found).
|
||||
*/
|
||||
function search_api_facetapi_get_taxonomy_hierarchy(array $values) {
|
||||
$values = array_filter($values, 'is_numeric');
|
||||
return $values ? facetapi_get_taxonomy_hierarchy($values) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map callback for all search_api facet fields.
|
||||
*
|
||||
* @param array $values
|
||||
* The values to map.
|
||||
* @param array $options
|
||||
* An associative array containing:
|
||||
* - field: Field information, as stored in the index, but with an additional
|
||||
* "key" property set to the field's internal name.
|
||||
* - index id: The machine name of the index for this facet.
|
||||
* - map callback: (optional) A callback that will be called at the beginning,
|
||||
* which allows initial mapping of filters. Only values not mapped by that
|
||||
* callback will be processed by this method.
|
||||
* - value callback: A callback used to map single values and the limits of
|
||||
* ranges. The signature is the same as for this function, but all values
|
||||
* will be single values.
|
||||
* - missing label: (optional) The label used for the "missing" facet.
|
||||
*
|
||||
* @return array
|
||||
* An array mapping raw filter values to their labels.
|
||||
*/
|
||||
function search_api_facetapi_facet_map_callback(array $values, array $options = array()) {
|
||||
$map = array();
|
||||
// See if we have an additional map callback.
|
||||
if (isset($options['map callback']) && is_callable($options['map callback'])) {
|
||||
$map = call_user_func($options['map callback'], $values, $options);
|
||||
}
|
||||
|
||||
// Then look at all unmapped values and save information for them.
|
||||
$mappable_values = array();
|
||||
$ranges = array();
|
||||
foreach ($values as $value) {
|
||||
$value = (string) $value;
|
||||
if (isset($map[$value])) {
|
||||
continue;
|
||||
}
|
||||
if ($value == '!') {
|
||||
// The "missing" filter is usually always the same, but we allow an easy
|
||||
// override via the "missing label" map option.
|
||||
$map['!'] = isset($options['missing label']) ? $options['missing label'] : '(' . t('none') . ')';
|
||||
continue;
|
||||
}
|
||||
$length = strlen($value);
|
||||
if ($length > 5 && $value[0] == '[' && $value[$length - 1] == ']' && ($pos = strpos($value, ' TO '))) {
|
||||
// This is a range filter.
|
||||
$lower = trim(substr($value, 1, $pos));
|
||||
$upper = trim(substr($value, $pos + 4, -1));
|
||||
if ($lower != '*') {
|
||||
$mappable_values[$lower] = TRUE;
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$mappable_values[$upper] = TRUE;
|
||||
}
|
||||
$ranges[$value] = array(
|
||||
'lower' => $lower,
|
||||
'upper' => $upper,
|
||||
);
|
||||
}
|
||||
else {
|
||||
// A normal, single-value filter.
|
||||
$mappable_values[$value] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mappable_values) {
|
||||
$map += call_user_func($options['value callback'], array_keys($mappable_values), $options);
|
||||
}
|
||||
|
||||
foreach ($ranges as $value => $range) {
|
||||
$lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
|
||||
$upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
|
||||
if ($lower == '*' && $upper == '*') {
|
||||
$map[$value] = t('any');
|
||||
}
|
||||
elseif ($lower == '*') {
|
||||
$map[$value] = "< $upper";
|
||||
}
|
||||
elseif ($upper == '*') {
|
||||
$map[$value] = "> $lower";
|
||||
}
|
||||
else {
|
||||
$map[$value] = "$lower – $upper";
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a human-readable label for single facet filter values.
|
||||
*
|
||||
* @param array $values
|
||||
* The values for which labels should be returned.
|
||||
* @param array $options
|
||||
* An associative array containing the following information about the facet:
|
||||
* - field: Field information, as stored in the index, but with an additional
|
||||
* "key" property set to the field's internal name.
|
||||
* - index id: The machine name of the index for this facet.
|
||||
* - map callback: (optional) A callback that will be called at the beginning,
|
||||
* which allows initial mapping of filters. Only values not mapped by that
|
||||
* callback will be processed by this method.
|
||||
* - value callback: A callback used to map single values and the limits of
|
||||
* ranges. The signature is the same as for this function, but all values
|
||||
* will be single values.
|
||||
* - missing label: (optional) The label used for the "missing" facet.
|
||||
*
|
||||
* @return array
|
||||
* An array mapping raw facet values to their labels.
|
||||
*/
|
||||
function _search_api_facetapi_facet_create_label(array $values, array $options) {
|
||||
$field = $options['field'];
|
||||
$map = array();
|
||||
$n = count($values);
|
||||
|
||||
// For entities, we can simply use the entity labels.
|
||||
if (isset($field['entity_type'])) {
|
||||
$type = $field['entity_type'];
|
||||
$entities = entity_load($type, $values);
|
||||
foreach ($entities as $id => $entity) {
|
||||
$label = entity_label($type, $entity);
|
||||
if ($label) {
|
||||
$map[$id] = $label;
|
||||
}
|
||||
}
|
||||
if (count($map) == $n) {
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
||||
// Then, we check whether there is an options list for the field.
|
||||
$index = search_api_index_load($options['index id']);
|
||||
$wrapper = $index->entityWrapper();
|
||||
$values = drupal_map_assoc($values);
|
||||
foreach (explode(':', $field['key']) as $part) {
|
||||
if (!isset($wrapper->$part)) {
|
||||
$wrapper = NULL;
|
||||
break;
|
||||
}
|
||||
$wrapper = $wrapper->$part;
|
||||
while (($info = $wrapper->info()) && search_api_is_list_type($info['type'])) {
|
||||
$wrapper = $wrapper[0];
|
||||
}
|
||||
}
|
||||
if ($wrapper && ($options_list = $wrapper->optionsList('view'))) {
|
||||
// We have no use for empty strings, as then the facet links would be
|
||||
// invisible.
|
||||
$map += array_intersect_key(array_filter($options_list, 'strlen'), $values);
|
||||
if (count($map) == $n) {
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
|
||||
// As a "last resort" we try to create a label based on the field type, for
|
||||
// all values that haven't got a mapping yet.
|
||||
foreach (array_diff_key($values, $map) as $value) {
|
||||
switch ($field['type']) {
|
||||
case 'boolean':
|
||||
$map[$value] = $value ? t('true') : t('false');
|
||||
break;
|
||||
case 'date':
|
||||
$v = is_numeric($value) ? $value : strtotime($value);
|
||||
$map[$value] = format_date($v, 'short');
|
||||
break;
|
||||
case 'duration':
|
||||
$map[$value] = format_interval($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function search_api_facetapi_form_search_api_admin_index_fields_alter(&$form, &$form_state) {
|
||||
$form['#submit'][] = 'search_api_facetapi_search_api_admin_index_fields_submit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for search_api_admin_index_fields().
|
||||
*/
|
||||
function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_state) {
|
||||
// Clears this searcher's cached facet definitions.
|
||||
$cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
|
||||
cache_clear_all($cid, 'cache', TRUE);
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
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.
|
||||
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.
|
||||
|
||||
Access features
|
||||
---------------
|
||||
Search views created with this module contain two query settings (located in
|
||||
the "Advanced" fieldset) which let you control the access checks executed for
|
||||
search results displayed in the view.
|
||||
|
||||
- Bypass access checks
|
||||
This option allows you to deactivate access filters that would otherwise be
|
||||
added to the search, if the index supports this. This is, for instance, the case
|
||||
for indexes on the "Node" item type, when the "Node access" data alteration is
|
||||
activated.
|
||||
Use this either to slightly speed up searches where additional checks are
|
||||
unnecessary (e.g., because you already filter on "Node: Published") and there is
|
||||
no other node access mechanism on your site) or to show certain data that users
|
||||
normally wouldn't have access to (e.g., a list of all matching node titles,
|
||||
published or not).
|
||||
|
||||
- Additional access checks on result entities
|
||||
When this option is activated, all result entities will be passed to an
|
||||
additional access check, even if search-time access checks are available for
|
||||
this index. The advantage is that access rules are guaranteed to be enforced –
|
||||
stale data in the index, which might make other access checks incorrect, won't
|
||||
influence this access check. You can also use it for item types for which no
|
||||
other access mechanisms are available.
|
||||
However, note that results filtered out this way will mess up paging, result
|
||||
counts and possibly other things too (like facet counts), as the result row is
|
||||
only hidden from display after the search has been executed. Where possible,
|
||||
you should therefore only use this in combination with appropriate filter
|
||||
settings ensuring that only when the index isn't up-to-date items will be
|
||||
filtered out this way.
|
||||
This option is only available for indexes on entity types.
|
||||
|
||||
Other features
|
||||
--------------
|
||||
- Change parse mode
|
||||
You can determine how search keys entered by the user will be parsed by going to
|
||||
"Advanced" > "Query settings" within your View's settings. "Direct" can be
|
||||
useful, e.g., when you want to give users the full power of Solr. In other
|
||||
cases, "Multiple terms" is usually what you want / what users expect.
|
||||
Caution: For letting users use fulltext searches, always use the "Search:
|
||||
Fulltext search" filter or contextual filter – using a normal filter on a
|
||||
fulltext field won't parse the search keys, which means multiple words will only
|
||||
be found when they appear as that exact phrase.
|
||||
|
||||
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.
|
@@ -0,0 +1,276 @@
|
||||
<?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 query(){
|
||||
parent::query();
|
||||
|
||||
$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'];
|
||||
}
|
||||
|
||||
$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_api_base_path'] = $base_path;
|
||||
$this->view->query->range(0, 0);
|
||||
}
|
||||
|
||||
public function render() {
|
||||
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;
|
||||
}
|
||||
|
||||
$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' => $this->view->query->getOption('search_api_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;
|
||||
}
|
||||
|
||||
return array(
|
||||
'facets' => array(
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $facets,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(){
|
||||
$info['content'] = $this->render();
|
||||
$info['content']['more'] = $this->render_more_link();
|
||||
$info['subject'] = filter_xss_admin($this->view->get_title());
|
||||
return $info;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerArgument.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The operator to use for multiple arguments.
|
||||
*
|
||||
* Either "and" or "or".
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see views_break_phrase
|
||||
*/
|
||||
public $operator;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$options['not'] = 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',
|
||||
);
|
||||
|
||||
$form['not'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Exclude'),
|
||||
'#description' => t('If selected, the numbers entered for the filter will be excluded rather than limiting the view.'),
|
||||
'#default_value' => !empty($this->options['not']),
|
||||
'#fieldset' => 'more',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
if (empty($this->value)) {
|
||||
if (!empty($this->options['break_phrase'])) {
|
||||
views_break_phrase($this->argument, $this);
|
||||
}
|
||||
else {
|
||||
$this->value = array($this->argument);
|
||||
}
|
||||
}
|
||||
|
||||
$operator = empty($this->options['not']) ? '=' : '<>';
|
||||
|
||||
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, $operator);
|
||||
}
|
||||
$this->query->filter($filter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->query->condition($this->real_field, reset($this->value), $operator);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiViewsHandlerArgumentDate class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines a contextual filter searching for a date or date range.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentDate extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
if (empty($this->value)) {
|
||||
$this->fillValue();
|
||||
if ($this->value === FALSE) {
|
||||
$this->abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$outer_conjunction = strtoupper($this->operator);
|
||||
|
||||
if (empty($this->options['not'])) {
|
||||
$operator = '=';
|
||||
$inner_conjunction = 'OR';
|
||||
}
|
||||
else {
|
||||
$operator = '<>';
|
||||
$inner_conjunction = 'AND';
|
||||
}
|
||||
|
||||
if (!empty($this->value)) {
|
||||
if (!empty($this->value)) {
|
||||
$outer_filter = $this->query->createFilter($outer_conjunction);
|
||||
foreach ($this->value as $value) {
|
||||
$value_filter = $this->query->createFilter($inner_conjunction);
|
||||
$values = explode(';', $value);
|
||||
$values = array_map(array($this, 'getTimestamp'), $values);
|
||||
if (in_array(FALSE, $values, TRUE)) {
|
||||
$this->abort();
|
||||
return;
|
||||
}
|
||||
$is_range = (count($values) > 1);
|
||||
|
||||
$inner_filter = ($is_range ? $this->query->createFilter('AND') : $value_filter);
|
||||
$range_op = (empty($this->options['not']) ? '>=' : '<');
|
||||
$inner_filter->condition($this->real_field, $values[0], $is_range ? $range_op : $operator);
|
||||
if ($is_range) {
|
||||
$range_op = (empty($this->options['not']) ? '<=' : '>');
|
||||
$inner_filter->condition($this->real_field, $values[1], $range_op);
|
||||
$value_filter->filter($inner_filter);
|
||||
}
|
||||
$outer_filter->filter($value_filter);
|
||||
}
|
||||
|
||||
$this->query->filter($outer_filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value to a timestamp, if it isn't one already.
|
||||
*
|
||||
* @param string|int $value
|
||||
* The value to convert. Either a timestamp, or a date/time string as
|
||||
* recognized by strtotime().
|
||||
*
|
||||
* @return int|false
|
||||
* The parsed timestamp, or FALSE if an illegal string was passed.
|
||||
*/
|
||||
public function getTimestamp($value) {
|
||||
if (is_numeric($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return strtotime($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills $this->value with data from the argument.
|
||||
*/
|
||||
protected function fillValue() {
|
||||
if (!empty($this->options['break_phrase'])) {
|
||||
// Set up defaults:
|
||||
if (!isset($this->value)) {
|
||||
$this->value = array();
|
||||
}
|
||||
|
||||
if (!isset($this->operator)) {
|
||||
$this->operator = 'OR';
|
||||
}
|
||||
|
||||
if (empty($this->argument)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preg_match('/^([-\d;:\s]+\+)*[-\d;:\s]+$/', $this->argument)) {
|
||||
// The '+' character in a query string may be parsed as ' '.
|
||||
$this->value = explode('+', $this->argument);
|
||||
}
|
||||
elseif (preg_match('/^([-\d;:\s]+,)*[-\d;:\s]+$/', $this->argument)) {
|
||||
$this->operator = 'AND';
|
||||
$this->value = explode(',', $this->argument);
|
||||
}
|
||||
|
||||
// Keep an 'error' value if invalid strings were given.
|
||||
if (!empty($this->argument) && (empty($this->value) || !is_array($this->value))) {
|
||||
$this->value = FALSE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->value = array($this->argument);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts the associated query due to an illegal argument.
|
||||
*/
|
||||
protected function abort() {
|
||||
$variables['!field'] = $this->definition['group'] . ': ' . $this->definition['title'];
|
||||
$this->query->abort(t('Illegal argument passed to !field contextual filter.', $variables));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the title this argument will assign the view, given the argument.
|
||||
*
|
||||
* @return string
|
||||
* A title fitting for the passed argument.
|
||||
*/
|
||||
public function title() {
|
||||
if (!empty($this->argument)) {
|
||||
if (empty($this->value)) {
|
||||
$this->fillValue();
|
||||
}
|
||||
$dates = array();
|
||||
foreach ($this->value as $date) {
|
||||
$date_parts = explode(';', $date);
|
||||
|
||||
$ts = $this->getTimestamp($date_parts[0]);
|
||||
$datestr = format_date($ts, 'short');
|
||||
if (count($date_parts) > 1) {
|
||||
$ts = $this->getTimestamp($date_parts[1]);
|
||||
$datestr .= ' - ' . format_date($ts, 'short');
|
||||
}
|
||||
|
||||
if ($datestr) {
|
||||
$dates[] = $datestr;
|
||||
}
|
||||
}
|
||||
|
||||
return $dates ? implode(', ', $dates) : check_plain($this->argument);
|
||||
}
|
||||
|
||||
return check_plain($this->argument);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerArgumentFulltext.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
$options['fields'] = array('default' => array());
|
||||
$options['conjunction'] = array('default' => 'AND');
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$form['help']['#markup'] = t('Note: You can change how search keys are parsed under "Advanced" > "Query settings".');
|
||||
|
||||
$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'],
|
||||
);
|
||||
$form['conjunction'] = array(
|
||||
'#title' => t('Operator'),
|
||||
'#description' => t('Determines how multiple keywords entered for the search will be combined.'),
|
||||
'#type' => 'radios',
|
||||
'#options' => array(
|
||||
'AND' => t('Contains all of these words'),
|
||||
'OR' => t('Contains any of these words'),
|
||||
),
|
||||
'#default_value' => $this->options['conjunction'],
|
||||
);
|
||||
|
||||
}
|
||||
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']);
|
||||
}
|
||||
if ($this->options['conjunction'] != 'AND') {
|
||||
$this->query->setOption('conjunction', $this->options['conjunction']);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerArgumentMoreLikeThis.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
unset($options['not']);
|
||||
$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']);
|
||||
unset($form['not']);
|
||||
|
||||
$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);
|
||||
watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.',
|
||||
array('@class' => $class['name']), WATCHDOG_ERROR);
|
||||
$this->query->abort();
|
||||
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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerArgumentString.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling string fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentString extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
if (empty($this->value)) {
|
||||
if (!empty($this->options['break_phrase'])) {
|
||||
views_break_phrase_string($this->argument, $this);
|
||||
}
|
||||
else {
|
||||
$this->value = array($this->argument);
|
||||
}
|
||||
}
|
||||
|
||||
parent::query($group_by);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiViewsHandlerArgumentTaxonomyTerm class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines a contextual filter searching through all indexed taxonomy fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentTaxonomyTerm extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
if (empty($this->value)) {
|
||||
$this->fillValue();
|
||||
}
|
||||
|
||||
$outer_conjunction = strtoupper($this->operator);
|
||||
|
||||
if (empty($this->options['not'])) {
|
||||
$operator = '=';
|
||||
$inner_conjunction = 'OR';
|
||||
}
|
||||
else {
|
||||
$operator = '<>';
|
||||
$inner_conjunction = 'AND';
|
||||
}
|
||||
|
||||
if (!empty($this->value)) {
|
||||
$terms = entity_load('taxonomy_term', $this->value);
|
||||
|
||||
if (!empty($terms)) {
|
||||
$filter = $this->query->createFilter($outer_conjunction);
|
||||
$vocabulary_fields = $this->definition['vocabulary_fields'];
|
||||
$vocabulary_fields += array('' => array());
|
||||
foreach ($terms as $term) {
|
||||
$inner_filter = $filter;
|
||||
if ($outer_conjunction != $inner_conjunction) {
|
||||
$inner_filter = $this->query->createFilter($inner_conjunction);
|
||||
}
|
||||
// Set filters for all term reference fields which don't specify a
|
||||
// vocabulary, as well as for all fields specifying the term's
|
||||
// vocabulary.
|
||||
if (!empty($this->definition['vocabulary_fields'][$term->vocabulary_machine_name])) {
|
||||
foreach ($this->definition['vocabulary_fields'][$term->vocabulary_machine_name] as $field) {
|
||||
$inner_filter->condition($field, $term->tid, $operator);
|
||||
}
|
||||
}
|
||||
foreach ($vocabulary_fields[''] as $field) {
|
||||
$inner_filter->condition($field, $term->tid, $operator);
|
||||
}
|
||||
if ($outer_conjunction != $inner_conjunction) {
|
||||
$filter->filter($inner_filter);
|
||||
}
|
||||
}
|
||||
|
||||
$this->query->filter($filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title this argument will assign the view, given the argument.
|
||||
*/
|
||||
public function title() {
|
||||
if (!empty($this->argument)) {
|
||||
if (empty($this->value)) {
|
||||
$this->fillValue();
|
||||
}
|
||||
$terms = array();
|
||||
foreach ($this->value as $tid) {
|
||||
$taxonomy_term = taxonomy_term_load($tid);
|
||||
if ($taxonomy_term) {
|
||||
$terms[] = check_plain($taxonomy_term->name);
|
||||
}
|
||||
}
|
||||
|
||||
return $terms ? implode(', ', $terms) : check_plain($this->argument);
|
||||
}
|
||||
else {
|
||||
return check_plain($this->argument);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill $this->value with data from the argument.
|
||||
*
|
||||
* Uses views_break_phrase(), if appropriate.
|
||||
*/
|
||||
protected function fillValue() {
|
||||
if (!empty($this->options['break_phrase'])) {
|
||||
views_break_phrase($this->argument, $this);
|
||||
}
|
||||
else {
|
||||
$this->value = array($this->argument);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 less than'),
|
||||
'<=' => t('Is less 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) && count($this->value) < 2) {
|
||||
$this->value = $this->value ? reset($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.
|
||||
if (empty($form_state['exposed'])) {
|
||||
$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'),
|
||||
);
|
||||
}
|
||||
elseif (!empty($this->options['expose']['use_operator'])) {
|
||||
$name = $this->options['expose']['operator_id'];
|
||||
$form['value']['#states']['visible'] = array(
|
||||
':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'),
|
||||
':input[name="' . $name . '"],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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterBoolean.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 : '',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterDate.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterEntity.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler class for entities.
|
||||
*
|
||||
* Should be extended for specific entity types, such as
|
||||
* SearchApiViewsHandlerFilterUser and SearchApiViewsHandlerFilterTaxonomyTerm.
|
||||
*
|
||||
* Based on views_handler_filter_term_node_tid.
|
||||
*/
|
||||
abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* If exposed form input was successfully validated, the entered entity IDs.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $validated_exposed_input;
|
||||
|
||||
/**
|
||||
* Validates entered entity labels and converts them to entity IDs.
|
||||
*
|
||||
* Since this can come from either the form or the exposed filter, this is
|
||||
* abstracted out a bit so it can handle the multiple input sources.
|
||||
*
|
||||
* @param array $form
|
||||
* The form or form element for which any errors should be set.
|
||||
* @param array $values
|
||||
* The entered user names to validate.
|
||||
*
|
||||
* @return array
|
||||
* The entity IDs corresponding to all entities that could be found.
|
||||
*/
|
||||
abstract protected function validate_entity_strings(array &$form, array $values);
|
||||
|
||||
/**
|
||||
* Transforms an array of entity IDs into a comma-separated list of labels.
|
||||
*
|
||||
* @param array $ids
|
||||
* The entity IDs to transform.
|
||||
*
|
||||
* @return string
|
||||
* A string containing the labels corresponding to the IDs, separated by
|
||||
* commas.
|
||||
*/
|
||||
abstract protected function ids_to_strings(array $ids);
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function operator_options() {
|
||||
$operators = array(
|
||||
'=' => $this->isMultiValued() ? t('Is one of') : t('Is'),
|
||||
'all of' => t('Is all of'),
|
||||
'<>' => $this->isMultiValued() ? t('Is not one of') : t('Is not'),
|
||||
'empty' => t('Is empty'),
|
||||
'not empty' => t('Is not empty'),
|
||||
);
|
||||
if (!$this->isMultiValued()) {
|
||||
unset($operators['all of']);
|
||||
}
|
||||
return $operators;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['expose']['multiple']['default'] = TRUE;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
|
||||
if (!is_array($this->value)) {
|
||||
$this->value = $this->value ? array($this->value) : array();
|
||||
}
|
||||
|
||||
// Set the correct default value in case the admin-set value is used (and a
|
||||
// value is present). The value is used if the form is either not exposed,
|
||||
// or the exposed form wasn't submitted yet (there is
|
||||
if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) {
|
||||
$form['value']['#default_value'] = $this->ids_to_strings($this->value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function value_validate($form, &$form_state) {
|
||||
if (!empty($form['value'])) {
|
||||
$value = &$form_state['values']['options']['value'];
|
||||
$values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value);
|
||||
$ids = $this->validate_entity_strings($form['value'], $values);
|
||||
|
||||
if ($ids) {
|
||||
$value = $ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function accept_exposed_input($input) {
|
||||
$rc = parent::accept_exposed_input($input);
|
||||
|
||||
if ($rc) {
|
||||
// If we have previously validated input, override.
|
||||
if ($this->validated_exposed_input) {
|
||||
$this->value = $this->validated_exposed_input;
|
||||
}
|
||||
}
|
||||
|
||||
return $rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exposed_validate(&$form, &$form_state) {
|
||||
if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$identifier = $this->options['expose']['identifier'];
|
||||
$input = $form_state['values'][$identifier];
|
||||
|
||||
if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
|
||||
$this->operator = $this->options['group_info']['group_items'][$input]['operator'];
|
||||
$input = $this->options['group_info']['group_items'][$input]['value'];
|
||||
}
|
||||
|
||||
$values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input);
|
||||
|
||||
if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) {
|
||||
$this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values);
|
||||
}
|
||||
else {
|
||||
$this->validated_exposed_input = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether multiple user names can be entered into this filter.
|
||||
*
|
||||
* This is either the case if the form isn't exposed, or if the " Allow
|
||||
* multiple selections" option is enabled.
|
||||
*
|
||||
* @param array $options
|
||||
* (optional) The options array to use. If not supplied, the options set on
|
||||
* this filter will be used.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if multiple values can be entered for this filter, FALSE otherwise.
|
||||
*/
|
||||
protected function isMultiValued(array $options = array()) {
|
||||
$options = $options ? $options : $this->options;
|
||||
return empty($options['exposed']) || !empty($options['expose']['multiple']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function admin_summary() {
|
||||
$value = $this->value;
|
||||
$this->value = empty($value) ? '' : $this->ids_to_strings($value);
|
||||
$ret = parent::admin_summary();
|
||||
$this->value = $value;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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']);
|
||||
}
|
||||
elseif (is_array($this->value)) {
|
||||
$all_of = $this->operator === 'all of';
|
||||
$operator = $all_of ? '=' : $this->operator;
|
||||
if (count($this->value) == 1) {
|
||||
$this->query->condition($this->real_field, reset($this->value), $operator, $this->options['group']);
|
||||
}
|
||||
else {
|
||||
$filter = $this->query->createFilter($operator === '<>' || $all_of ? 'AND' : 'OR');
|
||||
foreach ($this->value as $value) {
|
||||
$filter->condition($this->real_field, $value, $operator);
|
||||
}
|
||||
$this->query->filter($filter, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterFulltext.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterText {
|
||||
|
||||
/**
|
||||
* Displays the operator form, adding a description.
|
||||
*/
|
||||
public function show_operator_form(&$form, &$form_state) {
|
||||
$this->operator_form($form, $form_state);
|
||||
$form['operator']['#description'] = t('This operator is only useful when using \'Search keys\'.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array(
|
||||
'AND' => t('Contains all of these words'),
|
||||
'OR' => t('Contains any of these words'),
|
||||
'NOT' => t('Contains none of these words'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['operator']['default'] = 'AND';
|
||||
|
||||
$options['mode'] = array('default' => 'keys');
|
||||
$options['min_length'] = array('default' => '');
|
||||
$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. You can change how search keys are parsed under "Advanced" > "Query settings".'),
|
||||
'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;
|
||||
}
|
||||
|
||||
$form['min_length'] = array(
|
||||
'#title' => t('Minimum keyword length'),
|
||||
'#description' => t('Minimum length of each word in the search keys. Leave empty to allow all words.'),
|
||||
'#type' => 'textfield',
|
||||
'#element_validate' => array('element_validate_integer_positive'),
|
||||
'#default_value' => $this->options['min_length'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exposed_validate(&$form, &$form_state) {
|
||||
// Only validate exposed input.
|
||||
if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only need to validate if there is a minimum word length set.
|
||||
if ($this->options['min_length'] < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
$identifier = $this->options['expose']['identifier'];
|
||||
$input = &$form_state['values'][$identifier];
|
||||
|
||||
if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
|
||||
$this->operator = $this->options['group_info']['group_items'][$input]['operator'];
|
||||
$input = &$this->options['group_info']['group_items'][$input]['value'];
|
||||
}
|
||||
|
||||
// If there is no input, we're fine.
|
||||
if (!trim($input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$words = preg_split('/\s+/', $input);
|
||||
foreach ($words as $i => $word) {
|
||||
if (drupal_strlen($word) < $this->options['min_length']) {
|
||||
unset($words[$i]);
|
||||
}
|
||||
}
|
||||
if (!$words) {
|
||||
$vars['@count'] = $this->options['min_length'];
|
||||
$msg = t('You must include at least one positive keyword with @count characters or more.', $vars);
|
||||
form_error($form[$identifier], $msg);
|
||||
}
|
||||
$input = implode(' ', $words);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// If the operator was set to OR or NOT, set OR as the conjunction. (It is
|
||||
// also set for NOT since otherwise it would be "not all of these words".)
|
||||
if ($this->operator != 'AND') {
|
||||
$this->query->setOption('conjunction', $this->operator);
|
||||
}
|
||||
|
||||
$this->query->fields($fields);
|
||||
$old = $this->query->getOriginalKeys();
|
||||
$this->query->keys($this->value);
|
||||
if ($this->operator == 'NOT') {
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
<?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;
|
||||
|
||||
if (!is_array($this->value)) {
|
||||
$this->value = $this->value ? array($this->value) : array();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiViewsHandlerFilterOptions class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler for fields with a limited set of possible values.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Stores the values which are available on the form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $value_options = NULL;
|
||||
|
||||
/**
|
||||
* The type of form element used to display the options.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $value_form_type = 'checkboxes';
|
||||
|
||||
/**
|
||||
* Retrieves a wrapper for this filter's field.
|
||||
*
|
||||
* @return EntityMetadataWrapper|null
|
||||
* A wrapper for the field which this filter uses.
|
||||
*/
|
||||
protected function get_wrapper() {
|
||||
if ($this->query) {
|
||||
$index = $this->query->getIndex();
|
||||
}
|
||||
elseif (substr($this->view->base_table, 0, 17) == 'search_api_index_') {
|
||||
$index = search_api_index_load(substr($this->view->base_table, 17));
|
||||
}
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
$wrapper = $index->entityWrapper(NULL, TRUE);
|
||||
$parts = explode(':', $this->real_field);
|
||||
foreach ($parts as $i => $part) {
|
||||
if (!isset($wrapper->$part)) {
|
||||
return NULL;
|
||||
}
|
||||
$wrapper = $wrapper->$part;
|
||||
$info = $wrapper->info();
|
||||
if ($i < count($parts) - 1) {
|
||||
// Unwrap lists.
|
||||
$level = search_api_list_nesting_level($info['type']);
|
||||
for ($j = 0; $j < $level; ++$j) {
|
||||
$wrapper = $wrapper[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the value_options property with all possible options.
|
||||
*/
|
||||
protected function get_value_options() {
|
||||
if (isset($this->value_options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wrapper = $this->get_wrapper();
|
||||
if ($wrapper) {
|
||||
$this->value_options = $wrapper->optionsList('view');
|
||||
}
|
||||
else {
|
||||
$this->value_options = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
$options = array(
|
||||
'=' => t('Is one of'),
|
||||
'all of' => t('Is all of'),
|
||||
'<>' => t('Is none of'),
|
||||
'empty' => t('Is empty'),
|
||||
'not empty' => t('Is not empty'),
|
||||
);
|
||||
// "Is all of" doesn't make sense for single-valued fields.
|
||||
if (empty($this->definition['multi-valued'])) {
|
||||
unset($options['all of']);
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
foreach ($this->value_options as $id => $option) {
|
||||
if (!isset($this->options['value'][$id])) {
|
||||
unset($this->value_options[$id]);
|
||||
}
|
||||
}
|
||||
return $this->value_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) {
|
||||
$this->get_value_options();
|
||||
if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
|
||||
$options = $this->reduce_value_options();
|
||||
}
|
||||
else {
|
||||
$options = $this->value_options;
|
||||
}
|
||||
|
||||
$form['value'] = array(
|
||||
'#type' => $this->value_form_type,
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#options' => $options,
|
||||
'#multiple' => TRUE,
|
||||
'#size' => min(4, count($options)),
|
||||
'#default_value' => is_array($this->value) ? $this->value : array(),
|
||||
);
|
||||
|
||||
// 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.
|
||||
if (empty($form_state['exposed'])) {
|
||||
$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'),
|
||||
);
|
||||
}
|
||||
elseif (!empty($this->options['expose']['use_operator'])) {
|
||||
$name = $this->options['expose']['operator_id'];
|
||||
$form['value']['#states']['visible'] = array(
|
||||
':input[name="' . $name . '"],dummy-empty' => array('!value' => 'empty'),
|
||||
':input[name="' . $name . '"],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.
|
||||
$this->get_value_options();
|
||||
foreach ($this->value as $i => $value) {
|
||||
if (!isset($this->value_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) {
|
||||
switch ($this->operator) {
|
||||
case '=':
|
||||
case 'all of':
|
||||
$operator = '=';
|
||||
break;
|
||||
|
||||
case '<>':
|
||||
$operator = '<>';
|
||||
break;
|
||||
}
|
||||
// If there is only a single value, use just the plain operator, = or <>.
|
||||
$operator = check_plain($operator);
|
||||
$values = check_plain($this->value_options[reset($this->value)]);
|
||||
}
|
||||
else {
|
||||
foreach ($this->value as $value) {
|
||||
if ($values !== '') {
|
||||
$values .= ', ';
|
||||
}
|
||||
if (drupal_strlen($values) > 20) {
|
||||
$values .= '…';
|
||||
break;
|
||||
}
|
||||
$values .= check_plain($this->value_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']);
|
||||
return;
|
||||
}
|
||||
if ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the value.
|
||||
while (is_array($this->value) && count($this->value) == 1) {
|
||||
$this->value = reset($this->value);
|
||||
}
|
||||
|
||||
// Determine operator and conjunction. The defaults are already right for
|
||||
// "all of".
|
||||
$operator = '=';
|
||||
$conjunction = 'AND';
|
||||
switch ($this->operator) {
|
||||
case '=':
|
||||
$conjunction = 'OR';
|
||||
break;
|
||||
|
||||
case '<>':
|
||||
$operator = '<>';
|
||||
break;
|
||||
}
|
||||
|
||||
// If the value is an empty array, we either want no filter at all (for
|
||||
// "is none of"), or want to find only items with no value for the field.
|
||||
if ($this->value === array()) {
|
||||
if ($operator != '<>') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_scalar($this->value) && $this->value !== '') {
|
||||
$this->query->condition($this->real_field, $this->value, $operator, $this->options['group']);
|
||||
}
|
||||
elseif ($this->value) {
|
||||
$filter = $this->query->createFilter($conjunction);
|
||||
// $filter will be NULL if there were errors in the query.
|
||||
if ($filter) {
|
||||
foreach ($this->value as $v) {
|
||||
$filter->condition($this->real_field, $v, $operator);
|
||||
}
|
||||
$this->query->filter($filter, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterTaxonomyTerm.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler class for taxonomy term entities.
|
||||
*
|
||||
* Based on views_handler_filter_term_node_tid.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilterEntity {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function has_extra_options() {
|
||||
return !empty($this->definition['vocabulary']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['type'] = array('default' => !empty($this->definition['vocabulary']) ? 'textfield' : 'select');
|
||||
$options['hierarchy'] = array('default' => 0);
|
||||
$options['error_message'] = array('default' => TRUE, 'bool' => TRUE);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function extra_options_form(&$form, &$form_state) {
|
||||
$form['type'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Selection type'),
|
||||
'#options' => array('select' => t('Dropdown'), 'textfield' => t('Autocomplete')),
|
||||
'#default_value' => $this->options['type'],
|
||||
);
|
||||
|
||||
$form['hierarchy'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Show hierarchy in dropdown'),
|
||||
'#default_value' => !empty($this->options['hierarchy']),
|
||||
);
|
||||
$form['hierarchy']['#states']['visible'][':input[name="options[type]"]']['value'] = 'select';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
|
||||
if (!empty($this->definition['vocabulary'])) {
|
||||
$vocabulary = taxonomy_vocabulary_machine_name_load($this->definition['vocabulary']);
|
||||
$title = t('Select terms from vocabulary @voc', array('@voc' => $vocabulary->name));
|
||||
}
|
||||
else {
|
||||
$vocabulary = FALSE;
|
||||
$title = t('Select terms');
|
||||
}
|
||||
$form['value']['#title'] = $title;
|
||||
|
||||
if ($vocabulary && $this->options['type'] == 'textfield') {
|
||||
$form['value']['#autocomplete_path'] = 'admin/views/ajax/autocomplete/taxonomy/' . $vocabulary->vid;
|
||||
}
|
||||
else {
|
||||
if ($vocabulary && !empty($this->options['hierarchy'])) {
|
||||
$tree = taxonomy_get_tree($vocabulary->vid);
|
||||
$options = array();
|
||||
|
||||
if ($tree) {
|
||||
foreach ($tree as $term) {
|
||||
$choice = new stdClass();
|
||||
$choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
|
||||
$options[] = $choice;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$options = array();
|
||||
$query = db_select('taxonomy_term_data', 'td');
|
||||
$query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
|
||||
$query->fields('td');
|
||||
$query->orderby('tv.weight');
|
||||
$query->orderby('tv.name');
|
||||
$query->orderby('td.weight');
|
||||
$query->orderby('td.name');
|
||||
$query->addTag('term_access');
|
||||
if ($vocabulary) {
|
||||
$query->condition('tv.machine_name', $vocabulary->machine_name);
|
||||
}
|
||||
$result = $query->execute();
|
||||
foreach ($result as $term) {
|
||||
$options[$term->tid] = $term->name;
|
||||
}
|
||||
}
|
||||
|
||||
$default_value = (array) $this->value;
|
||||
|
||||
if (!empty($form_state['exposed'])) {
|
||||
$identifier = $this->options['expose']['identifier'];
|
||||
|
||||
if (!empty($this->options['expose']['reduce'])) {
|
||||
$options = $this->reduce_value_options($options);
|
||||
|
||||
if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
|
||||
$default_value = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->options['expose']['multiple'])) {
|
||||
if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
|
||||
$default_value = 'All';
|
||||
}
|
||||
elseif (empty($default_value)) {
|
||||
$keys = array_keys($options);
|
||||
$default_value = array_shift($keys);
|
||||
}
|
||||
// Due to #1464174 there is a chance that array('') was saved in the
|
||||
// admin ui. Let's choose a safe default value.
|
||||
elseif ($default_value == array('')) {
|
||||
$default_value = 'All';
|
||||
}
|
||||
else {
|
||||
$copy = $default_value;
|
||||
$default_value = array_shift($copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
$form['value']['#type'] = 'select';
|
||||
$form['value']['#multiple'] = TRUE;
|
||||
$form['value']['#options'] = $options;
|
||||
$form['value']['#size'] = min(9, count($options));
|
||||
$form['value']['#default_value'] = $default_value;
|
||||
|
||||
if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) {
|
||||
$form_state['input'][$identifier] = $default_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the available exposed options according to the selection.
|
||||
*/
|
||||
protected function reduce_value_options(array $options) {
|
||||
foreach ($options as $id => $option) {
|
||||
if (empty($this->options['value'][$id])) {
|
||||
unset($options[$id]);
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function value_validate($form, &$form_state) {
|
||||
// We only validate if they've chosen the text field style.
|
||||
if ($this->options['type'] != 'textfield') {
|
||||
return;
|
||||
}
|
||||
|
||||
parent::value_validate($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function accept_exposed_input($input) {
|
||||
if (empty($this->options['exposed'])) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// If view is an attachment and is inheriting exposed filters, then assume
|
||||
// exposed input has already been validated.
|
||||
if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) {
|
||||
$this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
|
||||
}
|
||||
|
||||
// If it's non-required and there's no value don't bother filtering.
|
||||
if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return parent::accept_exposed_input($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exposed_validate(&$form, &$form_state) {
|
||||
if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only validate if they've chosen the text field style.
|
||||
if ($this->options['type'] != 'textfield') {
|
||||
$input = $form_state['values'][$this->options['expose']['identifier']];
|
||||
if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
|
||||
$input = $this->options['group_info']['group_items'][$input]['value'];
|
||||
}
|
||||
|
||||
if ($input != 'All') {
|
||||
$this->validated_exposed_input = (array) $input;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parent::exposed_validate($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function validate_entity_strings(array &$form, array $values) {
|
||||
if (empty($values)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$tids = array();
|
||||
$names = array();
|
||||
$missing = array();
|
||||
foreach ($values as $value) {
|
||||
$missing[strtolower($value)] = TRUE;
|
||||
$names[] = $value;
|
||||
}
|
||||
|
||||
if (!$names) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$query = db_select('taxonomy_term_data', 'td');
|
||||
$query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
|
||||
$query->fields('td');
|
||||
$query->condition('td.name', $names);
|
||||
if (!empty($this->definition['vocabulary'])) {
|
||||
$query->condition('tv.machine_name', $this->definition['vocabulary']);
|
||||
}
|
||||
$query->addTag('term_access');
|
||||
$result = $query->execute();
|
||||
foreach ($result as $term) {
|
||||
unset($missing[strtolower($term->name)]);
|
||||
$tids[] = $term->tid;
|
||||
}
|
||||
|
||||
if ($missing) {
|
||||
if (!empty($this->options['error_message'])) {
|
||||
form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing)))));
|
||||
}
|
||||
else {
|
||||
// Add a bogus TID which will show an empty result for a positive filter
|
||||
// and be ignored for an excluding one.
|
||||
$tids[] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $tids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function expose_form(&$form, &$form_state) {
|
||||
parent::expose_form($form, $form_state);
|
||||
if ($this->options['type'] != 'select') {
|
||||
unset($form['expose']['reduce']);
|
||||
}
|
||||
$form['error_message'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Display error message'),
|
||||
'#description' => t('Display an error message if one of the entered terms could not be found.'),
|
||||
'#default_value' => !empty($this->options['error_message']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function ids_to_strings(array $ids) {
|
||||
return implode(', ', db_select('taxonomy_term_data', 'td')
|
||||
->fields('td', array('name'))
|
||||
->condition('td.tid', array_filter($ids))
|
||||
->execute()
|
||||
->fetchCol());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterText.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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"));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerFilterUser.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling user entities.
|
||||
*
|
||||
* Based on views_handler_filter_user_name.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
|
||||
// Set autocompletion.
|
||||
$path = $this->isMultiValued() ? 'admin/views/ajax/autocomplete/user' : 'user/autocomplete';
|
||||
$form['value']['#autocomplete_path'] = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function ids_to_strings(array $ids) {
|
||||
$names = array();
|
||||
$args[':uids'] = array_filter($ids);
|
||||
$result = db_query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args);
|
||||
$result = $result->fetchAllKeyed();
|
||||
foreach ($ids as $uid) {
|
||||
if (!$uid) {
|
||||
$names[] = variable_get('anonymous', t('Anonymous'));
|
||||
}
|
||||
elseif (isset($result[$uid])) {
|
||||
$names[] = $result[$uid];
|
||||
}
|
||||
}
|
||||
return implode(', ', $names);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function validate_entity_strings(array &$form, array $values) {
|
||||
$uids = array();
|
||||
$missing = array();
|
||||
foreach ($values as $value) {
|
||||
if (drupal_strtolower($value) === drupal_strtolower(variable_get('anonymous', t('Anonymous')))) {
|
||||
$uids[] = 0;
|
||||
}
|
||||
else {
|
||||
$missing[strtolower($value)] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$missing) {
|
||||
return $uids;
|
||||
}
|
||||
|
||||
$result = db_query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => array_values($missing)));
|
||||
foreach ($result as $account) {
|
||||
unset($missing[strtolower($account->name)]);
|
||||
$uids[] = $account->uid;
|
||||
}
|
||||
|
||||
if ($missing) {
|
||||
form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', $missing))));
|
||||
}
|
||||
|
||||
return $uids;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsHandlerSort.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiViewsCache class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin class for caching Search API views.
|
||||
*/
|
||||
class SearchApiViewsCache extends views_plugin_cache_time {
|
||||
|
||||
/**
|
||||
* Static cache for get_results_key().
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $_results_key = NULL;
|
||||
|
||||
/**
|
||||
* Static cache for getSearchApiQuery().
|
||||
*
|
||||
* @var SearchApiQueryInterface
|
||||
*/
|
||||
protected $search_api_query = NULL;
|
||||
|
||||
/**
|
||||
* Overrides views_plugin_cache::cache_set().
|
||||
*
|
||||
* Also stores Search API's internal search results.
|
||||
*/
|
||||
public function cache_set($type) {
|
||||
if ($type != 'results') {
|
||||
return parent::cache_set($type);
|
||||
}
|
||||
|
||||
$cid = $this->get_results_key();
|
||||
$data = array(
|
||||
'result' => $this->view->result,
|
||||
'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0,
|
||||
'current_page' => $this->view->get_current_page(),
|
||||
'search_api results' => $this->view->query->getSearchApiResults(),
|
||||
);
|
||||
cache_set($cid, $data, $this->table, $this->cache_set_expire($type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides views_plugin_cache::cache_get().
|
||||
*
|
||||
* Additionally stores successfully retrieved results with
|
||||
* search_api_current_search().
|
||||
*/
|
||||
public function cache_get($type) {
|
||||
if ($type != 'results') {
|
||||
return parent::cache_get($type);
|
||||
}
|
||||
|
||||
// Values to set: $view->result, $view->total_rows, $view->execute_time,
|
||||
// $view->current_page.
|
||||
if ($cache = cache_get($this->get_results_key(), $this->table)) {
|
||||
$cutoff = $this->cache_expire($type);
|
||||
if (!$cutoff || $cache->created > $cutoff) {
|
||||
$this->view->result = $cache->data['result'];
|
||||
$this->view->total_rows = $cache->data['total_rows'];
|
||||
$this->view->set_current_page($cache->data['current_page']);
|
||||
$this->view->execute_time = 0;
|
||||
|
||||
// Trick Search API into believing a search happened, to make facetting
|
||||
// et al. work.
|
||||
$query = $this->getSearchApiQuery();
|
||||
search_api_current_search($query->getOption('search id'), $query, $cache->data['search_api results']);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides views_plugin_cache::get_results_key().
|
||||
*
|
||||
* Use the Search API query as the main source for the key.
|
||||
*/
|
||||
public function get_results_key() {
|
||||
global $user;
|
||||
|
||||
if (!isset($this->_results_key)) {
|
||||
$query = $this->getSearchApiQuery();
|
||||
$query->preExecute();
|
||||
$key_data = array(
|
||||
'query' => $query,
|
||||
'roles' => array_keys($user->roles),
|
||||
'super-user' => $user->uid == 1, // special caching for super user.
|
||||
'language' => $GLOBALS['language']->language,
|
||||
'base_url' => $GLOBALS['base_url'],
|
||||
);
|
||||
// Not sure what gets passed in exposed_info, so better include it. All
|
||||
// other parameters used in the parent method are already reflected in the
|
||||
// Search API query object we use.
|
||||
if (isset($_GET['exposed_info'])) {
|
||||
$key_data['exposed_info'] = $_GET['exposed_info'];
|
||||
}
|
||||
|
||||
$this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data));
|
||||
}
|
||||
|
||||
return $this->_results_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Search API query object associated with the current view.
|
||||
*
|
||||
* @return SearchApiQueryInterface|null
|
||||
* The Search API query object associated with the current view; or NULL if
|
||||
* there is none.
|
||||
*/
|
||||
protected function getSearchApiQuery() {
|
||||
if (!isset($this->search_api_query)) {
|
||||
$this->search_api_query = FALSE;
|
||||
if (isset($this->view->query) && $this->view->query instanceof SearchApiViewsQuery) {
|
||||
$this->search_api_query = $this->view->query->getSearchApiQuery();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->search_api_query ? $this->search_api_query : NULL;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,683 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains SearchApiViewsQuery.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Whether to abort the search instead of executing it.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $abort = FALSE;
|
||||
|
||||
/**
|
||||
* 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' => $this->options['parse_mode'],
|
||||
));
|
||||
}
|
||||
}
|
||||
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 some access options.
|
||||
*/
|
||||
public function option_definition() {
|
||||
return parent::option_definition() + array(
|
||||
'search_api_bypass_access' => array(
|
||||
'default' => FALSE,
|
||||
),
|
||||
'entity_access' => array(
|
||||
'default' => FALSE,
|
||||
),
|
||||
'parse_mode' => array(
|
||||
'default' => 'terms',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'],
|
||||
);
|
||||
|
||||
if (entity_get_info($this->index->item_type)) {
|
||||
$form['entity_access'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Additional access checks on result entities'),
|
||||
'#description' => t("Execute an access check for all result entities. This prevents users from seeing inappropriate content when the index contains stale data, or doesn't provide access checks. However, result counts, paging and other things won't work correctly if results are eliminated in this way, so only use this as a last ressort (and in addition to other checks, if possible)."),
|
||||
'#default_value' => $this->options['entity_access'],
|
||||
);
|
||||
}
|
||||
|
||||
$form['parse_mode'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Parse mode'),
|
||||
'#description' => t('Choose how the search keys will be parsed.'),
|
||||
'#options' => array(),
|
||||
'#default_value' => $this->options['parse_mode'],
|
||||
);
|
||||
foreach ($this->query->parseModes() as $key => $mode) {
|
||||
$form['parse_mode']['#options'][$key] = $mode['name'];
|
||||
if (!empty($mode['description'])) {
|
||||
$states['visible'][':input[name="query[options][parse_mode]"]']['value'] = $key;
|
||||
$form["parse_mode_{$key}_description"] = array(
|
||||
'#type' => 'item',
|
||||
'#title' => $mode['name'],
|
||||
'#description' => $mode['description'],
|
||||
'#states' => $states,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'])) {
|
||||
$group += array('type' => 'AND');
|
||||
// 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();
|
||||
|
||||
// Set the search ID, if it was not already set.
|
||||
if ($this->query->getOption('search id') == get_class($this->query)) {
|
||||
$this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// If the View and the Panel conspire to provide an overridden path then
|
||||
// pass that through as the base path.
|
||||
if (!empty($this->view->override_path) && strpos(current_path(), $this->view->override_path) !== 0) {
|
||||
$this->query->setOption('search_api_base_path', $this->view->override_path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alter(&$view) {
|
||||
parent::alter($view);
|
||||
drupal_alter('search_api_views_query', $view, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || $this->abort) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Calculate the "skip result count" option, if it wasn't already set to
|
||||
// FALSE.
|
||||
$skip_result_count = $this->query->getOption('skip result count', TRUE);
|
||||
if ($skip_result_count) {
|
||||
$skip_result_count = !$this->pager->use_count_query() && empty($view->get_total_rows);
|
||||
$this->query->setOption('skip result count', $skip_result_count);
|
||||
}
|
||||
|
||||
try {
|
||||
// Trigger pager pre_execute().
|
||||
$this->pager->pre_execute($this->query);
|
||||
|
||||
// Views passes sometimes NULL and sometimes the integer 0 for "All" in a
|
||||
// pager. If set to 0 items, a string "0" is passed. Therefore, we unset
|
||||
// the limit if an empty value OTHER than a string "0" was passed.
|
||||
if (!$this->limit && $this->limit !== '0') {
|
||||
$this->limit = NULL;
|
||||
}
|
||||
// Set the range. (We always set this, as there might even be an offset if
|
||||
// all items are shown.)
|
||||
$this->query->range($this->offset, $this->limit);
|
||||
|
||||
$start = microtime(TRUE);
|
||||
|
||||
// Execute the search.
|
||||
$results = $this->query->execute();
|
||||
$this->search_api_results = $results;
|
||||
|
||||
// Store the results.
|
||||
if (!$skip_result_count) {
|
||||
$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;
|
||||
|
||||
// Trigger pager post_execute().
|
||||
$this->pager->post_execute($view->result);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
// Recursion to get the same error behaviour as above.
|
||||
return $this->execute($view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this search query.
|
||||
*
|
||||
* Used by handlers to flag a fatal error which shouldn't be displayed but
|
||||
* still lead to the view returning empty and the search not being executed.
|
||||
*
|
||||
* @param string|null $msg
|
||||
* Optionally, a translated, unescaped error message to display.
|
||||
*/
|
||||
public function abort($msg = NULL) {
|
||||
if ($msg) {
|
||||
$this->errors[] = $msg;
|
||||
}
|
||||
$this->abort = TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!empty($this->options['entity_access'])) {
|
||||
$entity = entity_load($this->index->item_type, array($id));
|
||||
if (!entity_access('view', $this->index->item_type, $entity[$id])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$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 $i => $wrapper) {
|
||||
try {
|
||||
// Get the entity ID beforehand for possible watchdog messages.
|
||||
$id = $wrapper->value(array('identifier' => TRUE));
|
||||
|
||||
// Only add results that exist.
|
||||
if ($entity = $wrapper->value()) {
|
||||
$return[$i] = $entity;
|
||||
}
|
||||
else {
|
||||
watchdog('search_api_views', 'The search index returned a reference to an entity with ID @id, which does not exist in the database. Your index may be out of sync and should be rebuilt.', array('@id' => $id), WATCHDOG_ERROR);
|
||||
}
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
watchdog_exception('search_api_views', $e, "%type while trying to load search result entity with ID @id: !message in %function (line %line of %file).", array('@id' => $id), WATCHDOG_ERROR);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
$entity_type = $this->index->getEntityType();
|
||||
$wrappers = array();
|
||||
$load_entities = array();
|
||||
foreach ($results as $row_index => $row) {
|
||||
if ($entity_type && 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($entity_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 = $entity_type ? $entity_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', $tags = array()) {
|
||||
if (!$this->errors) {
|
||||
return $this->query->createFilter($conjunction, $tags);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Search Views module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alter the query before executing the query.
|
||||
*
|
||||
* @param view $view
|
||||
* The view object about to be processed.
|
||||
* @param SearchApiViewsQuery $query
|
||||
* The Search API Views query to be altered.
|
||||
*
|
||||
* @see hook_views_query_alter()
|
||||
*/
|
||||
function hook_search_api_views_query_alter(view &$view, SearchApiViewsQuery &$query) {
|
||||
// (Example assuming a view with an exposed filter on node title.)
|
||||
// If the input for the title filter is a positive integer, filter against
|
||||
// node ID instead of node title.
|
||||
if ($view->name == 'my_view' && is_numeric($view->exposed_raw_input['title']) && $view->exposed_raw_input['title'] > 0) {
|
||||
// Traverse through the 'where' part of the query.
|
||||
foreach ($query->where as &$condition_group) {
|
||||
foreach ($condition_group['conditions'] as &$condition) {
|
||||
// If this is the part of the query filtering on title, chang the
|
||||
// condition to filter on node ID.
|
||||
if (reset($condition) == 'node.title') {
|
||||
$condition = array('node.nid', $view->exposed_raw_input['title'],'=');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
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/plugins
|
||||
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_string.inc
|
||||
files[] = includes/handler_argument_date.inc
|
||||
files[] = includes/handler_argument_taxonomy_term.inc
|
||||
files[] = includes/handler_filter.inc
|
||||
files[] = includes/handler_filter_boolean.inc
|
||||
files[] = includes/handler_filter_date.inc
|
||||
files[] = includes/handler_filter_entity.inc
|
||||
files[] = includes/handler_filter_fulltext.inc
|
||||
files[] = includes/handler_filter_language.inc
|
||||
files[] = includes/handler_filter_options.inc
|
||||
files[] = includes/handler_filter_taxonomy_term.inc
|
||||
files[] = includes/handler_filter_text.inc
|
||||
files[] = includes/handler_filter_user.inc
|
||||
files[] = includes/handler_sort.inc
|
||||
files[] = includes/plugin_cache.inc
|
||||
files[] = includes/query.inc
|
||||
|
||||
; Information added by Drupal.org packaging script on 2013-12-25
|
||||
version = "7.x-1.11"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1387965506"
|
||||
|
@@ -0,0 +1,105 @@
|
||||
<?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 $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 &$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.
|
||||
*
|
||||
* @param $field
|
||||
* Some data to be searched for field names that should be altered. Passed by
|
||||
* reference.
|
||||
* @param array $fields
|
||||
* An array mapping Search API field identifiers (as previously used by Views)
|
||||
* to the new, sanitized Views field identifiers.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if any data was changed, FALSE otherwise.
|
||||
*/
|
||||
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');
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Integrates the Search API with Views.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
// 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) {
|
||||
// Check whether index was disabled.
|
||||
if (!$index->enabled && $index->original->enabled) {
|
||||
_search_api_views_index_unavailable($index);
|
||||
}
|
||||
|
||||
// Check whether the indexed fields changed.
|
||||
$old_fields = $index->original->options + array('fields' => array());
|
||||
$old_fields = $old_fields['fields'];
|
||||
$new_fields = $index->options + array('fields' => array());
|
||||
$new_fields = $new_fields['fields'];
|
||||
if ($old_fields != $new_fields) {
|
||||
views_invalidate_cache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Views hook implementations for the Search API Views module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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->getEntityType()])) {
|
||||
$table['table'] += array(
|
||||
'entity type' => $index->getEntityType(),
|
||||
'skip entity load' => TRUE,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$wrapper = $index->entityWrapper(NULL, FALSE);
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$wrapper = $index->entityWrapper(NULL);
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
watchdog_exception('search_api_views', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
||||
// If there are taxonomy term references indexed in the index, include the
|
||||
// "Indexed taxonomy term fields" contextual filter. We also save for all
|
||||
// fields whether they contain only terms of a certain vocabulary, keying
|
||||
// that information by vocabulary for later ease of use.
|
||||
$vocabulary_fields = array();
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
if (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
|
||||
$field_id = ($pos = strrpos($key, ':')) ? substr($key, $pos + 1) : $key;
|
||||
$field_info = field_info_field($field_id);
|
||||
if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
|
||||
$vocabulary_fields[$field_info['settings']['allowed_values'][0]['vocabulary']][] = $key;
|
||||
}
|
||||
else {
|
||||
$vocabulary_fields[''][] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($vocabulary_fields) {
|
||||
$table['search_api_views_taxonomy_term']['group'] = t('Search');
|
||||
$table['search_api_views_taxonomy_term']['title'] = t('Indexed taxonomy term fields');
|
||||
$table['search_api_views_taxonomy_term']['help'] = t('Search in all indexed taxonomy term fields.');
|
||||
$table['search_api_views_taxonomy_term']['argument']['handler'] = 'SearchApiViewsHandlerArgumentTaxonomyTerm';
|
||||
$table['search_api_views_taxonomy_term']['argument']['vocabulary_fields'] = $vocabulary_fields;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
watchdog_exception('search_api_views', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds handler definitions for a field to a Views data table definition.
|
||||
*
|
||||
* Helper method for search_api_views_views_data().
|
||||
*
|
||||
* @param $id
|
||||
* The internal identifier of the field.
|
||||
* @param array $field
|
||||
* Information about the field.
|
||||
* @param EntityMetadataWrapper $wrapper
|
||||
* A wrapper providing further metadata about the field.
|
||||
* @param array $table
|
||||
* The existing Views data table definition, as a reference.
|
||||
*/
|
||||
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' => 'SearchApiViewsHandlerArgument',
|
||||
),
|
||||
'filter' => array(
|
||||
'handler' => 'SearchApiViewsHandlerFilterText',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$info = $wrapper->info();
|
||||
if (isset($info['options list']) && is_callable($info['options list'])) {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
|
||||
$table[$id]['filter']['multi-valued'] = search_api_is_list_type($type);
|
||||
}
|
||||
elseif ($inner_type == 'boolean') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterBoolean';
|
||||
}
|
||||
elseif ($inner_type == 'date') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
|
||||
}
|
||||
elseif (isset($field['entity_type']) && $field['entity_type'] === 'user') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterUser';
|
||||
}
|
||||
elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm';
|
||||
$info = $wrapper->info();
|
||||
$field_info = field_info_field($info['name']);
|
||||
// For the "Parent terms" and "All parent terms" properties, we can
|
||||
// extrapolate the vocabulary from the parent in the selector. (E.g.,
|
||||
// for "field_tags:parent" we can use the information of "field_tags".)
|
||||
// Otherwise, we can't include any vocabulary information.
|
||||
if (!$field_info && ($info['name'] == 'parent' || $info['name'] == 'parents_all')) {
|
||||
if (!empty($table[$id]['real field'])) {
|
||||
$parts = explode(':', $table[$id]['real field']);
|
||||
$field_info = field_info_field($parts[count($parts) - 2]);
|
||||
}
|
||||
}
|
||||
if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
|
||||
$table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
|
||||
}
|
||||
|
||||
if ($inner_type == 'string' || $inner_type == 'uri') {
|
||||
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentString';
|
||||
}
|
||||
elseif ($inner_type == 'date') {
|
||||
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgumentDate';
|
||||
}
|
||||
else {
|
||||
$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() {
|
||||
// Collect all base tables provided by this module.
|
||||
$bases = array();
|
||||
foreach (search_api_index_load_multiple(FALSE) as $index) {
|
||||
$bases[] = 'search_api_index_' . $index->machine_name;
|
||||
}
|
||||
|
||||
$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',
|
||||
),
|
||||
),
|
||||
'cache' => array(
|
||||
'search_api_views_cache' => array(
|
||||
'title' => t('Search-specific'),
|
||||
'help' => t("Cache Search API views. (Other methods probably won't work with search views.)"),
|
||||
'base' => $bases,
|
||||
'handler' => 'SearchApiViewsCache',
|
||||
'uses options' => TRUE,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user