From 8acdd4f840a1b1f368e52a236a8980b03c9bda67 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 13 Dec 2017 00:37:44 -0600 Subject: [PATCH 1/6] Merge PR #5 from official repository --- views_selective_filters.views_execution.inc | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 views_selective_filters.views_execution.inc diff --git a/views_selective_filters.views_execution.inc b/views_selective_filters.views_execution.inc new file mode 100644 index 0000000..ec8e0e6 --- /dev/null +++ b/views_selective_filters.views_execution.inc @@ -0,0 +1,45 @@ +filter as $filter_name => $filter) { + if (preg_match("/${missing}${field_suffix}$/", $filter_name) === 1) { + $uses_selective_filters = TRUE; + $replace_with = preg_replace("/${field_suffix}$/", '', $filter_name); + $look_for = preg_replace("/${missing}$/", '', $replace_with); + $replacement_map[$look_for] = $replace_with; + } + } + + if ($uses_selective_filters) { + foreach ($query->where as &$condition_group) { + foreach ($condition_group['conditions'] as &$condition) { + $exploded = explode('.', $condition['field']); + $search_key = array_pop($exploded); + if (isset($replacement_map[$search_key])) { + $condition['field'] = preg_replace("/.${search_key}$/", '.' . $replacement_map[$search_key], $condition['field']); + } + } + } + } +} From 6e008c321696ff949e2e018b4670a899915bb425 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 13 Dec 2017 00:40:42 -0600 Subject: [PATCH 2/6] Add .gitignore and composer.json --- .gitignore | 2 ++ composer.json | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c36e38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..133f7f3 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "drupal/views_selective_filters", + "description": "Restrict exposed filter values to those present in the result set.", + "type": "drupal-module", + "license": "GPL-2.0+", + "support": { + "issues": "https://drupal.org/project/issues/views_selective_filters", + "source": "https://drupal.org/project/views_selective_filters" + }, + "keywords": [ + "Drupal", + "Views", + "Filters" + ] +} From 24fc7afdb5300548f328813acfd7306be669e736 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Fri, 12 Jan 2018 11:30:45 -0600 Subject: [PATCH 3/6] by bmcclure: Added option to get labels from a specified entity type, refactored some of the module code. --- src/Plugin/views/filter/Selective.php | 982 ++++++++++++++------------ 1 file changed, 531 insertions(+), 451 deletions(-) diff --git a/src/Plugin/views/filter/Selective.php b/src/Plugin/views/filter/Selective.php index 7db8c96..4a0922d 100644 --- a/src/Plugin/views/filter/Selective.php +++ b/src/Plugin/views/filter/Selective.php @@ -8,6 +8,8 @@ namespace Drupal\views_selective_filters\Plugin\views\filter; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\filter\InOperator; @@ -23,480 +25,558 @@ */ class Selective extends InOperator { - /** - * The original filter value options, if it's an options list handler. - * - * @var array|false - */ - protected $originalOptions; - - protected static $results; - - /** - * {@inheritdoc} - */ - public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { - parent::init($view, $display, $options); - - $this->options['exposed'] = TRUE; - $this->realField = $this->options['selective_display_field']; - } - - /** - * {@inheritdoc} - */ - protected function defineOptions() { - $options = parent::defineOptions(); - - // Storage for field used to display values. - $options['selective_display_field']['default'] = ''; - // Storage for sort used to sort display values. - $options['selective_display_sort']['default'] = 'ASC'; - // Storage for aggregated fields - $options['selective_aggregated_fields']['default'] = ''; - // Limit aggregated items to prevent a huge number of options in select. - $options['selective_items_limit']['default'] = 100; - - return $options; - } - - /** - * {@inheritdoc} - */ - public function getValueOptions() { - if (isset($this->valueOptions)) { - return $this->valueOptions; + /** + * The original filter value options, if it's an options list handler. + * + * @var array|false + */ + protected $originalOptions; + + /** + * A static cache used for storing results. + * + * @var array + */ + protected static $results; + + /** + * {@inheritdoc} + */ + public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { + parent::init($view, $display, $options); + + $this->options['exposed'] = TRUE; + $this->realField = $this->options['selective_display_field']; } - $this->valueOptions = []; - - // If $this->view->selective_oids means that the handler is being called - // inside the cloned view used to obtain the selective values and thus this - // is to prevent infinite recursive loop. - if (empty($this->view->selective_oids) && !empty($this->view->inited)) { - $this->valueOptions = $this->getOids(); - // TODO: Omit null values in result: they are improperly handled. - // When constructing the query. - $this->valueOptions = array_diff_key($this->valueOptions, ['' => NULL]); - // Set a flag in the view so we know it is using selective filters. - $this->view->using_selective = TRUE; + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + // Storage for field used to display values. + $options['selective_display_field']['default'] = ''; + // Storage for optional entity type to get labels from. + $options['selective_entity_type']['default'] = ''; + // Storage for sort used to sort display values. + $options['selective_display_sort']['default'] = 'ASC'; + // Storage for aggregated fields + $options['selective_aggregated_fields']['default'] = ''; + // Limit aggregated items to prevent a huge number of options in select. + $options['selective_items_limit']['default'] = 100; + + return $options; } - else { - if (!empty($this->view->selective_oids)) { + + /** + * {@inheritdoc} + */ + public function getValueOptions() { + if (isset($this->valueOptions)) { + return $this->valueOptions; + } + $this->valueOptions = []; - } - else { - // This is a special case, if $this->valueOptions is not an array - // then parent::valueForm() will throw an exception, so, - // in our custom override no form is generated when $this->valueOptions - // is not an array. We only want this to happen in the administrative - // interface. - // unset($this->valueOptions); - } - } - return $this->valueOptions; - } - - /** - * {@inheritdoc} - */ - protected function valueForm(&$form, FormStateInterface $form_state) { - $this->getValueOptions(); - // If you call parent::valueForm() and $this->valueOptions - // is not an array, an exception is thrown. - if (isset($this->valueOptions) && is_array($this->valueOptions)) { - parent::valueForm($form, $form_state); - } - // Avoid the 'illegal values' Form API error. - $form['value']['#validated'] = TRUE; - // Add behaviour for ajax block refresh. - // Don't do this if the view is being executed - // to obtain selective values. - // if (empty($this->view->selective_oids)) { - // $form['#attached']['js'][] = drupal_get_path('module', 'views_filters_selective') . '/js/attachBehaviours.js'; - // } - } - - /** - * {@inheritdoc} - */ - public function validate() { - $this->getValueOptions(); - $errors = array(); - - // If the operator is an operator which doesn't require a value, there is - // no need for additional validation. - if (in_array($this->operator, $this->operatorValues(0))) { - return array(); - } + // If $this->view->selective_oids means that the handler is being called + // inside the cloned view used to obtain the selective values and thus this + // is to prevent infinite recursive loop. + if (empty($this->view->selective_oids) && !empty($this->view->inited)) { + $this->valueOptions = $this->getOids(); + // TODO: Omit null values in result: they are improperly handled. + // When constructing the query. + $this->valueOptions = array_diff_key($this->valueOptions, ['' => NULL]); + // Set a flag in the view so we know it is using selective filters. + $this->view->using_selective = TRUE; + } + else { + if (!empty($this->view->selective_oids)) { + $this->valueOptions = []; + } + else { + // This is a special case, if $this->valueOptions is not an array + // then parent::valueForm() will throw an exception, so, + // in our custom override no form is generated when $this->valueOptions + // is not an array. We only want this to happen in the administrative + // interface. + // unset($this->valueOptions); + } + } - if (!in_array($this->operator, $this->operatorValues(1))) { - $errors[] = $this->t('The operator is invalid on filter: @filter.', array('@filter' => $this->adminLabel(TRUE))); + return $this->valueOptions; } - if (is_array($this->value)) { - // This is overridden because it causes problems during preview. + + /** + * {@inheritdoc} + */ + protected function valueForm(&$form, FormStateInterface $form_state) { + $this->getValueOptions(); + // If you call parent::valueForm() and $this->valueOptions + // is not an array, an exception is thrown. + if (isset($this->valueOptions) && \is_array($this->valueOptions)) { + parent::valueForm($form, $form_state); + } + // Avoid the 'illegal values' Form API error. + $form['value']['#validated'] = TRUE; + // Add behaviour for ajax block refresh. + // Don't do this if the view is being executed + // to obtain selective values. + // if (empty($this->view->selective_oids)) { + // $form['#attached']['js'][] = drupal_get_path('module', 'views_filters_selective') . '/js/attachBehaviours.js'; + // } } - elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) { - $errors[] = $this->t('The value @value is not an array for @operator on filter: @filter', array('@value' => var_export($this->value), '@operator' => $this->operator, '@filter' => $this->adminLabel(TRUE))); + + /** + * {@inheritdoc} + */ + public function validate() { + $this->getValueOptions(); + $errors = array(); + + // If the operator is an operator which doesn't require a value, there is + // no need for additional validation. + if (in_array($this->operator, $this->operatorValues(0))) { + return array(); + } + + if (!in_array($this->operator, $this->operatorValues(1))) { + $errors[] = $this->t('The operator is invalid on filter: @filter.', array('@filter' => $this->adminLabel(TRUE))); + } + if (is_array($this->value)) { + // This is overridden because it causes problems during preview. + } + elseif (!empty($this->value) && ($this->operator == 'in' || $this->operator == 'not in')) { + $errors[] = $this->t('The value @value is not an array for @operator on filter: @filter', array('@value' => var_export($this->value), '@operator' => $this->operator, '@filter' => $this->adminLabel(TRUE))); + } + return $errors; } - return $errors; - } - - /** - * Checks if two base fields are compatible. - */ - protected function baseFieldCompatible($base_field1, $base_field2) { - return strpos($base_field2, $base_field1) === 0; - } - - /** - * {@inheritdoc} - */ - public function buildOptionsForm(&$form, FormStateInterface $form_state) { - $base_field = $this->definition['field_base']; - - parent::buildOptionsForm($form, $form_state); - // Filter should always be exposed, show warning. - array_unshift($form['expose_button'], array( - 'warning' => array( - '#theme' => 'status_messages', - '#message_list' => ['warning' => [t('This filter is always exposed to users.')]], - '#status_headings' => [ - 'status' => t('Status message'), - 'error' => t('Error message'), - 'warning' => t('Warning message'), - ], - ))); - // Remove option to unexpose filter. Tried to disable, but did not work. - $form['expose_button']['checkbox']['checkbox']['#type'] = 'hidden'; - unset($form['expose_button']['button']); - unset($form['expose_button']['markup']); - // Do not allow to check "all values". - $form['value']['#access'] = FALSE; - // Cannot group without values. - unset($form['group_button']); - - // Add combo to pick display field for filter. - $options = []; - foreach ($this->view->display_handler->getHandlers('field') as $field) { - if ($this->baseFieldCompatible($base_field, $field->field)) { - $options[$field->options['id']] = $field->adminLabel(); - } + + /** + * Checks if two base fields are compatible. + */ + protected function baseFieldCompatible($base_field1, $base_field2) { + return strpos($base_field2, $base_field1) === 0; } - $form['selective_display_field'] = array( - '#title' => t('Display field'), - '#type' => 'select', - '#description' => t('Field to be used for the selective options.'), - '#options' => $options, - '#default_value' => $this->options['selective_display_field'], - ); - - // Add combo to pick sort for display. - $options = []; - $options['NONE'] = t('No sorting'); - // Add option for custom sortings. - if ($this->getOriginalOptions()) { - $options['ORIG'] = t('As the original filter'); + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $base_field = $this->definition['field_base']; + + parent::buildOptionsForm($form, $form_state); + // Filter should always be exposed, show warning. + array_unshift($form['expose_button'], array( + 'warning' => array( + '#theme' => 'status_messages', + '#message_list' => ['warning' => [$this->t('This filter is always exposed to users.')]], + '#status_headings' => [ + 'status' => t('Status message'), + 'error' => t('Error message'), + 'warning' => t('Warning message'), + ], + ))); + // Remove option to unexpose filter. Tried to disable, but did not work. + $form['expose_button']['checkbox']['checkbox']['#type'] = 'hidden'; + // Do not allow to check "all values". + $form['value']['#access'] = FALSE; + // Remove unused form elements. + unset($form['expose_button']['button'], $form['expose_button']['markup'], $form['group_button']); + + // Add combo to pick display field for filter. + $options = []; + foreach ($this->view->display_handler->getHandlers('field') as $field) { + if ($this->baseFieldCompatible($base_field, $field->field)) { + $options[$field->options['id']] = $field->adminLabel(); + } + } + + $form['selective_display_field'] = [ + '#title' => $this->t('Display field'), + '#type' => 'select', + '#description' => $this->t('Field to be used for the selective options.'), + '#options' => $options, + '#default_value' => $this->options['selective_display_field'], + ]; + + $form['selective_entity_type'] = [ + '#title' => $this->t('Entity type'), + '#type' => 'select', + '#description' => $this->t('Optionally, select an entity type to look up labels from instead of using the field style. Field keys must be entity IDs.'), + '#options' => $this->getEntityTypeOptions(), + '#default_value' => $this->options['selective_entity_type'], + ]; + + // Add combo to pick sort for display. + $options = []; + $options['NONE'] = $this->t('No sorting'); + // Add option for custom sortings. + if ($this->getOriginalOptions()) { + $options['ORIG'] = $this->t('As the original filter'); + } + $options['KASC'] = $this->t('Custom key ascending (ksort)'); + $options['KDESC'] = $this->t('Custom key descending (ksort reverse)'); + $options['ASC'] = $this->t('Custom ascending (asort)'); + $options['DESC'] = $this->t('Custom descending (asort reverse)'); + // TODO: Allow the use of view's sorts! + //foreach ($this->view->display_handler->handlers['sort'] as $key => $handler) { + // $options[$handler->options['id']] = $handler->definition['group'] . ': ' . $handler->definition['title']; + //} + $form['selective_display_sort'] = array( + '#title' => $this->t('Sort field'), + '#type' => 'select', + '#description' => $this->t('Choose wich field to use for display'), + '#options' => $options, + '#default_value' => $this->options['selective_display_sort'], + ); + $form['selective_items_limit'] = array( + '#title' => $this->t('Limit number of select items'), + '#type' => 'textfield', + '#description' => $this->t("Don't allow a badly configured selective filter to return thousands of possible values. Enter a limit or remove any value for no limit. We recommend to set a limit no higher than 100."), + '#default_value' => $this->options['selective_items_limit'], + '#min' => 0, + ); } - $options['KASC'] = t('Custom key ascending (ksort)'); - $options['KDESC'] = t('Custom key descending (ksort reverse)'); - $options['ASC'] = t('Custom ascending (asort)'); - $options['DESC'] = t('Custom descending (asort reverse)'); - // TODO: Allow the use of view's sorts! - //foreach ($this->view->display_handler->handlers['sort'] as $key => $handler) { - // $options[$handler->options['id']] = $handler->definition['group'] . ': ' . $handler->definition['title']; - //} - $form['selective_display_sort'] = array( - '#title' => t('Sort field'), - '#type' => 'select', - '#description' => t('Choose wich field to use for display'), - '#options' => $options, - '#default_value' => $this->options['selective_display_sort'], - ); - $form['selective_items_limit'] = array( - '#title' => t('Limit number of select items'), - '#type' => 'textfield', - '#description' => t("Don't allow a badly configured selective filter to return thousands of possible values. Enter a limit or remove any value for no limit. We recommend to set a limit no higher than 100."), - '#default_value' => $this->options['selective_items_limit'], - '#min' => 0, - ); - } - - /** - * {@inheritdoc} - */ - public function buildExposeForm(&$form, FormStateInterface $form_state) { - parent::buildExposeForm($form, $form_state); - // Remove reduce resultset interface. - unset($form['expose']['reduce']); - // TODO: Populated somewhere through AJAX, I could not find it.... - // Provide default value for filter name. - if (empty($form['expose']['identifier']['#default_value'])) { - $form['expose']['identifier']['#default_value'] = $this->options['field']; + + /** + * Gets an array of all entity type options with label as value and entity + * type ID as the key. + * + * @return array + * The entity types array. + */ + protected function getEntityTypeOptions() { + $entity_types = \Drupal::entityTypeManager()->getDefinitions(); + $options = []; + + /** + * @var string $entity_type_id + * @var \Drupal\Core\Entity\EntityTypeInterface $entity_type + */ + foreach ($entity_types as $entity_type_id => $entity_type) { + if ($entity_type instanceof ContentEntityTypeInterface) { + $options[$entity_type_id] = $entity_type->getLabel(); + } + } + + return $options; } - if (empty($form['expose']['label']['#default_value'])) { - $form['expose']['label']['#default_value'] = $this->definition['title']; + + /** + * {@inheritdoc} + */ + public function buildExposeForm(&$form, FormStateInterface $form_state) { + parent::buildExposeForm($form, $form_state); + // Remove reduce resultset interface. + unset($form['expose']['reduce']); + // TODO: Populated somewhere through AJAX, I could not find it.... + // Provide default value for filter name. + if (empty($form['expose']['identifier']['#default_value'])) { + $form['expose']['identifier']['#default_value'] = $this->options['field']; + } + if (empty($form['expose']['label']['#default_value'])) { + $form['expose']['label']['#default_value'] = $this->definition['title']; + } + if (empty($form['ui_name']['#default_value'])) { + $form['ui_name']['#default_value'] = $this->definition['title']; + } } - if (empty($form['ui_name']['#default_value'])) { - $form['ui_name']['#default_value'] = $this->definition['title']; + + /** + * {@inheritdoc} + */ + public function query() { + // If this view was constructed to obtain the selective values for this + // handler, the handler should not add any constraints itself. + if (isset($this->view->selective_handler_signature) && $this->getSignature() === $this->view->selective_handler_signature) { + return; + } + + parent::query(); } - } - - /** - * {@inheritdoc} - */ - public function query() { - // If this view was constructed to obtain the selective values for this - // handler, the handler should not add any constraints itself. - if (isset($this->view->selective_handler_signature) && $this->getSignature() === $this->view->selective_handler_signature) { - return; + + /** + * Returns a signature for current filter handler. + * + * @return string + * The signature. + */ + protected function getSignature() { + return hash('sha256', serialize(array( + 'id' => $this->view->id(), + 'args' => $this->view->args, + 'input' => $this->view->getExposedInput(), + 'base_field' => $this->definition['field_base'], + 'real_field' => $this->realField, + 'field' => $this->field, + 'table' => $this->table, + 'ui_name' => $this->adminLabel(), + ))); } - parent::query(); - } - - /** - * Returns a signature for current filter handler. - * - * @return string - * The signature. - */ - protected function getSignature() { - return hash('sha256', serialize(array( - 'id' => $this->view->id(), - 'args' => $this->view->args, - 'input' => $this->view->getExposedInput(), - 'base_field' => $this->definition['field_base'], - 'real_field' => $this->realField, - 'field' => $this->field, - 'table' => $this->table, - 'ui_name' => $this->adminLabel(), - ))); - } - - /** - * Returns a list of options for current view, only at runtime. - */ - protected function getOids() { - // Parameters that we will be using during the process. - $base_field = $this->definition['field_base']; - $ui_name = $this->adminLabel(); - $signature = $this->getSignature(); - - // Prevent same filters from being recalculated. - if (empty(static::$results[$signature])) { - // We don't want a badly configured selective filter - // to return thousands of possible values. - $max_items = (int) $this->options['selective_items_limit']; - - // Clone the view (so it works while editting) and get all results. - $view_copy = Views::executableFactory()->get($this->view->storage); - if (!$view_copy) { - return NULL; - } - // Store a flag so that we can know from other places - // that this view is being used to obtain selective data. - $view_copy->selective_oids = TRUE; - // Store information about what filter is this view being used for. - $view_copy->selective_handler_signature = $signature; - // Transfer contextual information to cloned view. - $view_copy->setExposedInput($this->view->getExposedInput()); - $view_copy->setArguments($this->view->args); - - // Mess up with the field used for distinct have thousands of elements. - // Limit result set to 100: anything above is not user friendly at all. - $view_copy->setItemsPerPage($max_items); - - $view_copy->setDisplay($this->view->current_display); - $display = $view_copy->getDisplay(); - - // Remove any exposed form configuration. This showed up with BEF module! - unset($display->display_options['exposed_form']); - - $fields =& $display->getHandlers('field'); - - $fields = array_intersect_key($fields, [$this->options['selective_display_field'] => TRUE]); - - // Check to see if the user remembered to add the field. - if (empty($fields)) { - drupal_set_message(t('Selective query filter must have corresponding field added to view with Administrative Name set to "@name" and Base Type "@type"', - array( - '@name' => $ui_name, - '@type' => $base_field)), - 'error'); - return []; - } - - // Get ID of field that will be used for rendering. - $field = reset($fields); - - $field_options = $field->options; - - // Get field Id. - $field_id = $field_options['id']; - - // Check that relationships are coherent between Field and Filter. - $no_display_field_relationship = empty($field_options['relationship']) || $field_options['relationship'] === 'none'; - $no_filter_relationship = empty($this->options['relationship']) || $this->options['relationship'] === 'none'; - $equal - = (($no_display_field_relationship === TRUE) && ($no_filter_relationship === TRUE)) || - ($field_options['relationship'] === $this->options['relationship']); - - if (!$equal) { - drupal_set_message(t('Selective filter "@name": relationship of field and filter must match.', - array( - '@name' => $ui_name, - '@type' => $base_field)), - 'error'); - return []; - } - - // If main field is excluded from presentation, bring it back. - // Set group type for handler to populate database relationships in query. - $field_options['exclude'] = 0; - $field_options['group_type'] = 'group'; - - // Remove all sorting: sorts must be added to aggregate fields. - // $sorts =& $display->getHandlers('sort'); - // $sorts = []; - - // Turn this into an aggregate query. - $display->setOption('group_by', 1); - - // Aggregate is incompatible with distinct and pure distinct. - // At least it does not make sense as it is implemented now. - $query_options = $display->getOption('query'); - $query_options['options']['distinct'] = TRUE; - $display->setOption('query', $query_options); - - // Some style plugins can affect the built query, make sure we use a - // reliable field based style plugin. - $display->setOption('pager', ['type' => 'none', 'options' => []]); - $display->setOption('style', ['type' => 'default', 'options' => []]); - $display->setOption('row', ['type' => 'fields', 'options' => []]); - $display->setOption('cache', ['type' => 'none', 'options' => []]); - - // Run View. - $view_copy->execute($this->view->current_display); - - // We show human-readable values when case. - if (method_exists($field, 'getValueOptions')) { - $field->getValueOptions(); - } - - $style = $display->getPlugin('style'); - - // Create array of objects for selector. - $oids = []; - foreach ($view_copy->result as $row) { - $key = $field->getValue($row); - $key = is_array($key) ? reset($key) : $key; - // @todo This double escapes markup. - $value = $style->getField($row->index, $field_id); - $oids[$key] = SafeMarkup::checkPlain($value); - } - - // Sort values. - $sort_option = $this->options['selective_display_sort']; - switch($sort_option) { - case 'ASC': - asort($oids); - break; - - case 'DESC': - arsort($oids); - break; - - case 'KASC': - ksort($oids); - break; - - case 'KDESC': - krsort($oids); - break; - - case 'ORIG': - $oids = static::filterOriginalOptions($this->getOriginalOptions(), array_keys($oids)); - break; - - case 'NONE': - break; - - default: - asort($oids); - } - - // If limit exceeded this field is not good for being "selective". - if (!empty($max_items) && count($oids) == $max_items) { - drupal_set_message(t('Selective filter "@field" has limited the amount of total results. Please, review you query configuration.', array('@field' => $ui_name)), 'warning'); - } - - static::$results[$signature] = $oids; - $view_copy->destroy(); + protected function getViewCopy() { + // We don't want a badly configured selective filter + // to return thousands of possible values. + $max_items = (int) $this->options['selective_items_limit']; + + // Clone the view (so it works while editting) and get all results. + $view_copy = Views::executableFactory()->get($this->view->storage); + if (!$view_copy) { + return NULL; + } + // Store a flag so that we can know from other places + // that this view is being used to obtain selective data. + $view_copy->selective_oids = TRUE; + // Store information about what filter is this view being used for. + $view_copy->selective_handler_signature = $signature; + // Transfer contextual information to cloned view. + $view_copy->setExposedInput($this->view->getExposedInput()); + $view_copy->setArguments($this->view->args); + + // Mess up with the field used for distinct have thousands of elements. + // Limit result set to 100: anything above is not user friendly at all. + $view_copy->setItemsPerPage($max_items); + + $view_copy->setDisplay($this->view->current_display); + + return $view_copy; } - return static::$results[$signature]; - } - - /** - * Filters a list of original options according to selected set. - * - * @param array $options - * The options list of the original filter. - * @param array $set - * The narrowed set of results provided by the cloned view. - * - * @return array - * The original filter options list narrowed to the cloned query results. - */ - static protected function filterOriginalOptions($options, $set) { - $filtered = array(); - - foreach ($options as $key => $value) { - // Handle grouped options. - // @see hook_options_list(). - if (is_array($value)) { - $nested = static::filterOriginalOptions($value, $set); - if (!empty($nested)) { - $filtered[$key] = $nested; + /** + * Returns a list of options for current view, only at runtime. + */ + protected function getOids() { + // Parameters that we will be using during the process. + $base_field = $this->definition['field_base']; + $ui_name = $this->adminLabel(); + $signature = $this->getSignature(); + + // Prevent same filters from being recalculated. + if (empty(static::$results[$signature])) { + // We don't want a badly configured selective filter + // to return thousands of possible values. + $max_items = (int) $this->options['selective_items_limit']; + + // Clone the view (so it works while editting) and get all results. + $view_copy = $this->getViewCopy(); + if (!$view_copy) { + return NULL; + } + + $display = $view_copy->getDisplay(); + + // Remove any exposed form configuration. This showed up with BEF module! + unset($display->display_options['exposed_form']); + + $fields =& $display->getHandlers('field'); + $fields = array_intersect_key($fields, [$this->options['selective_display_field'] => TRUE]); + + // Check to see if the user remembered to add the field. + if (empty($fields)) { + drupal_set_message(t('Selective query filter must have corresponding field added to view with Administrative Name set to "@name" and Base Type "@type"', + array( + '@name' => $ui_name, + '@type' => $base_field)), + 'error'); + return []; + } + + // Get ID of field that will be used for rendering. + /** @var \Drupal\views\Plugin\views\field\FieldHandlerInterface $field */ + $field = reset($fields); + + $field_options = $field->options; + + // Get field Id. + $field_id = $field_options['id']; + + // Check that relationships are coherent between Field and Filter. + $no_display_field_relationship = empty($field_options['relationship']) || $field_options['relationship'] === 'none'; + $no_filter_relationship = empty($this->options['relationship']) || $this->options['relationship'] === 'none'; + $equal + = (($no_display_field_relationship === TRUE) && ($no_filter_relationship === TRUE)) || + ($field_options['relationship'] === $this->options['relationship']); + + if (!$equal) { + drupal_set_message(t('Selective filter "@name": relationship of field and filter must match.', + array( + '@name' => $ui_name, + '@type' => $base_field)), + 'error'); + return []; + } + + // If main field is excluded from presentation, bring it back. + // Set group type for handler to populate database relationships in query. + $field_options['exclude'] = 0; + $field_options['group_type'] = 'group'; + + // Remove all sorting: sorts must be added to aggregate fields. + // $sorts =& $display->getHandlers('sort'); + // $sorts = []; + + // Turn this into an aggregate query. + $display->setOption('group_by', 1); + + // Aggregate is incompatible with distinct and pure distinct. + // At least it does not make sense as it is implemented now. + $query_options = $display->getOption('query'); + $query_options['options']['distinct'] = TRUE; + $display->setOption('query', $query_options); + + // Some style plugins can affect the built query, make sure we use a + // reliable field based style plugin. + $display->setOption('pager', ['type' => 'none', 'options' => []]); + $display->setOption('style', ['type' => 'default', 'options' => []]); + $display->setOption('row', ['type' => 'fields', 'options' => []]); + $display->setOption('cache', ['type' => 'none', 'options' => []]); + + // Run View. + $view_copy->execute($this->view->current_display); + + // We show human-readable values when case. + if (method_exists($field, 'getValueOptions')) { + $field->getValueOptions(); + } + + /** @var \Drupal\views\Plugin\views\style\StylePluginBase $style */ + $style = $display->getPlugin('style'); + + // Create array of objects for selector. + $oids = []; + $entityTypeStorage = NULL; + if ($this->options['selective_entity_type']) { + $entityTypeStorage = \Drupal::entityTypeManager()->getStorage($this->options['selective_entity_type']); + } + + foreach ($view_copy->result as $row) { + $keys = $field->getValue($row); + + foreach ((array) $keys as $key) { + $value = NULL; + + if (NULL !== $entityTypeStorage) { + $entity = $entityTypeStorage->load($key); + + if ($entity) { + $value = $entity->label(); + } + } + else { + // @todo This double escapes markup. + $value = (string) $style->getField($row->index, $field_id); + } + + if (NULL !== $value) { + $oids[$key] = SafeMarkup::checkPlain($value); + } + } + } + + // Sort values. + $sort_option = $this->options['selective_display_sort']; + switch($sort_option) { + case 'ASC': + asort($oids); + break; + case 'DESC': + arsort($oids); + break; + case 'KASC': + ksort($oids); + break; + case 'KDESC': + krsort($oids); + break; + case 'ORIG': + $oids = static::filterOriginalOptions($this->getOriginalOptions(), array_keys($oids)); + break; + case 'NONE': + break; + default: + asort($oids); + } + + // If limit exceeded this field is not good for being "selective". + if (!empty($max_items) && count($oids) === $max_items) { + drupal_set_message(t('Selective filter "@field" has limited the amount of total results. Please, review you query configuration.', array('@field' => $ui_name)), 'warning'); + } + + static::$results[$signature] = $oids; + $view_copy->destroy(); } - continue; - } - if (in_array($key, $set)) { - $filtered[$key] = $value; - } + + return static::$results[$signature]; } - return $filtered; - } - - /** - * Returns the original filter value options, if provides an options list. - * - * @return array|false - * The original filter option list, if available, or FALSE. - */ - protected function getOriginalOptions() { - if (!isset($this->originalOptions)) { - // $this->originalOptions = FALSE; - // $class = $this->definition['proxy']; - // $original_filter = new $class([], '', []); - // if (is_callable(array($original_filter, 'getValueOptions'))) { - // $original_filter->set_definition($this->definition); - // $options = $original_filter->getValueOptions(); - // // We store only non-empty array. - // if (is_array($options) && !empty($options)) { - // $this->originalOptions = $options; - // } - // else { - // $this->originalOptions = array(); - // } - // } + /** + * Filters a list of original options according to selected set. + * + * @param array $options + * The options list of the original filter. + * @param array $set + * The narrowed set of results provided by the cloned view. + * + * @return array + * The original filter options list narrowed to the cloned query results. + */ + protected static function filterOriginalOptions($options, $set) { + $filtered = array(); + + foreach ($options as $key => $value) { + // Handle grouped options. + // @see hook_options_list(). + if (\is_array($value)) { + $nested = static::filterOriginalOptions($value, $set); + if (!empty($nested)) { + $filtered[$key] = $nested; + } + continue; + } + if (\in_array($key, $set, FALSE)) { + $filtered[$key] = $value; + } + } + + return $filtered; } - return $this->originalOptions; - } + /** + * Returns the original filter value options, if provides an options list. + * + * @return array|false + * The original filter option list, if available, or FALSE. + */ + protected function getOriginalOptions() { + if (!isset($this->originalOptions)) { + /** @var \Drupal\views\Plugin\ViewsHandlerManager $filterManager */ + $filterManager = \Drupal::service('plugin.manager.views.filter'); + $this->originalOptions = FALSE; + /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $original_filter */ + $original_filter = $filterManager->createInstance($this->definition['proxy'], $this->configuration); + $original_filter->definition = $this->getPluginDefinition(); + $original_filter->value = $this->value; + $original_filter->view = $this->view; + + if (is_callable(array($original_filter, 'getValueOptions'))) { + $options = $original_filter->getValueOptions(); + + // We store only non-empty array. + $this->originalOptions = (is_array($options) && !empty($options)) ? $options : []; + } + + if (empty($this->originalOptions)) { + $form = []; + $original_filter->buildExposedForm($form, new FormState()); + + if (isset($form['value']['#options'])) { + $this->originalOptions = $form['value']['#options']; + } + } + } + + return $this->originalOptions; + } } From 166e0534c008f2d314f10e9ae2c16e86bb31d09e Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Fri, 12 Jan 2018 11:41:35 -0600 Subject: [PATCH 4/6] by bmcclure: Add default None option to entity types. --- src/Plugin/views/filter/Selective.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/views/filter/Selective.php b/src/Plugin/views/filter/Selective.php index 4a0922d..64d3208 100644 --- a/src/Plugin/views/filter/Selective.php +++ b/src/Plugin/views/filter/Selective.php @@ -248,7 +248,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { */ protected function getEntityTypeOptions() { $entity_types = \Drupal::entityTypeManager()->getDefinitions(); - $options = []; + $options = ['' => '- None -']; /** * @var string $entity_type_id From 5821f94f69d27c4eb0f6e04485d0e5fd809903ae Mon Sep 17 00:00:00 2001 From: Miranda Jones <> Date: Tue, 23 Jan 2018 14:15:34 -0500 Subject: [PATCH 5/6] Fixed missing variable --- src/Plugin/views/filter/Selective.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/views/filter/Selective.php b/src/Plugin/views/filter/Selective.php index 64d3208..66e3523 100644 --- a/src/Plugin/views/filter/Selective.php +++ b/src/Plugin/views/filter/Selective.php @@ -329,7 +329,7 @@ protected function getViewCopy() { // that this view is being used to obtain selective data. $view_copy->selective_oids = TRUE; // Store information about what filter is this view being used for. - $view_copy->selective_handler_signature = $signature; + $view_copy->selective_handler_signature = $this->getSignature(); // Transfer contextual information to cloned view. $view_copy->setExposedInput($this->view->getExposedInput()); $view_copy->setArguments($this->view->args); From bc829023c1aeeafdaea61b1816c9fd5fbdfd2bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tassilo=20Gr=C3=B6per?= Date: Thu, 9 Apr 2020 16:45:42 +0200 Subject: [PATCH 6/6] fix drupal 8.8 compatability Throwing: Notice: Indirect modification of overloaded property Drupal\ctools_views\Plugin\Display\Block::$display_options has no effect in Drupal\views_selective_filters\Plugin\views\filter\Selective->getOids() (line 371 of modules/contrib/views_selective_filters/src/Plugin/views/filter/Selective.php). Drupal\views_selective_filters\Plugin\views\filter\Selective->getOids() (Line: 87) Drupal\views_selective_filters\Plugin\views\filter\Selective->getValueOptions() (Line: 115) Drupal\views_selective_filters\Plugin\views\filter\Selective->valueForm(Array, Object) (Line: 922) Drupal\views\Plugin\views\filter\FilterPluginBase->buildExposedForm(Array, Object) (Line: 115) Drupal\views\Form\ViewsExposedForm->buildForm(Array, Object) --- src/Plugin/views/filter/Selective.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/views/filter/Selective.php b/src/Plugin/views/filter/Selective.php index 66e3523..b654555 100644 --- a/src/Plugin/views/filter/Selective.php +++ b/src/Plugin/views/filter/Selective.php @@ -367,7 +367,7 @@ protected function getOids() { $display = $view_copy->getDisplay(); // Remove any exposed form configuration. This showed up with BEF module! - unset($display->display_options['exposed_form']); + unset($display->display['display_options']['exposed_form']); $fields =& $display->getHandlers('field'); $fields = array_intersect_key($fields, [$this->options['selective_display_field'] => TRUE]);