diff --git a/address.views.inc b/address.views.inc index 1fcafae2ef93bf555fe15f1c5db3244968624f56..25c83fb8b922daf7176ff227297c31bf31cd9632 100644 --- a/address.views.inc +++ b/address.views.inc @@ -43,6 +43,8 @@ function address_field_views_data(FieldStorageConfigInterface $field) { } // Add the custom country_code filter. $data[$table_name][$field_name . '_country_code']['filter']['id'] = 'country_code'; + // Add the custom administrative_area filter. + $data[$table_name][$field_name . '_administrative_area']['filter']['id'] = 'administrative_area'; } } elseif ($field_type == 'address_country') { diff --git a/config/schema/address.schema.yml b/config/schema/address.schema.yml index fe933986793b167ea32b45a0d196b51e386a657d..7f8cf16428269a338e9ec9df39e82d1919f99aba 100644 --- a/config/schema/address.schema.yml +++ b/config/schema/address.schema.yml @@ -150,3 +150,29 @@ field.widget.settings.address_zone_default: views.filter.country_code: type: views.filter.in_operator label: 'Country' + +views.filter.administrative_area: + type: views.filter.in_operator + label: 'Administrative area' + mapping: + country: + type: mapping + mapping: + country_source: + type: string + label: 'Country source' + country_argument_id: + type: string + label: 'Country contextual filter ID' + country_filter_id: + type: string + label: 'Exposed country filter ID' + country_static_code: + type: string + label: 'Predefined country for administrative areas' + expose: + type: mapping + mapping: + label_type: + type: string + label: 'Label type' diff --git a/src/Plugin/views/filter/AdministrativeArea.php b/src/Plugin/views/filter/AdministrativeArea.php new file mode 100644 index 0000000000000000000000000000000000000000..2670db6d2a3121d458b56cc9ce7b7765500829d5 --- /dev/null +++ b/src/Plugin/views/filter/AdministrativeArea.php @@ -0,0 +1,635 @@ +addressFormatRepository = $address_format_repository; + $this->subdivisionRepository = $subdivision_repository; + $this->formState = NULL; + $this->currentCountryCode = ''; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('address.country_repository'), + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('address.address_format_repository'), + $container->get('address.subdivision_repository') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['country'] = [ + 'contains' => [ + 'country_source' => ['default' => ''], + 'country_argument_id' => ['default' => ''], + 'country_filter_id' => ['default' => ''], + 'country_static_code' => ['default' => ''], + ], + ]; + $options['expose']['contains']['label_type']['default'] = 'static'; + + return $options; + } + + /** + * {@inheritdoc} + */ + protected function canBuildGroup() { + // To be able to define a group, you have to be able to select values + // while configuring the filter. But this filter doesn't let you select + // values until a country is selected, so the group filter functionality + // is impossible. + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->formState = $form_state; + + $form['country'] = [ + '#type' => 'container', + '#weight' => -300, + ]; + $form['country']['country_source'] = [ + '#type' => 'radios', + '#title' => $this->t('Country source'), + '#options' => [ + 'static' => $this->t('A predefined country code'), + 'argument' => $this->t('The value of a contextual filter'), + 'filter' => $this->t('The value of an exposed filter'), + ], + '#default_value' => $this->options['country']['country_source'], + '#ajax' => [ + 'callback' => [get_class($this), 'ajaxRefreshCountry'], + 'wrapper' => 'admin-area-value-options-ajax-wrapper', + ], + ]; + + $argument_options = []; + // Find all the contextual filters on the display to use as options. + foreach ($this->view->display_handler->getHandlers('argument') as $name => $argument) { + // @todo Limit this to arguments pointing to a country code field. + $argument_options[$name] = $argument->adminLabel(); + } + if (!empty($argument_options)) { + $form['country']['country_argument_id'] = [ + '#type' => 'select', + '#title' => t('Country contextual filter'), + '#options' => $argument_options, + '#default_value' => $this->options['country']['country_argument_id'], + ]; + } + else { + // #states doesn't work on markup elements, so use a container. + $form['country']['country_argument_id'] = [ + '#type' => 'container', + ]; + $form['country']['country_argument_id']['error'] = [ + '#type' => 'markup', + '#markup' => t('You must add a contextual filter for the country code to use this filter for administrative areas.'), + ]; + } + $form['country']['country_argument_id']['#states'] = [ + 'visible' => [ + ':input[name="options[country][country_source]"]' => ['value' => 'argument'], + ], + ]; + + $filter_options = []; + // Find all country_code filters from address.module for the valid choices. + foreach ($this->view->display_handler->getHandlers('filter') as $name => $filter) { + $definition = $filter->pluginDefinition; + if ($definition['id'] == 'country_code' && $definition['provider'] == 'address') { + $filter_options[$name] = $filter->adminLabel(); + } + } + if (!empty($filter_options)) { + $form['country']['country_filter_id'] = [ + '#type' => 'select', + '#title' => t('Exposed country filter to determine values'), + '#options' => $filter_options, + '#default_value' => $this->options['country']['country_filter_id'], + ]; + } + else { + // #states doesn't work on markup elements, so we to use a container. + $form['country']['country_filter_id'] = [ + '#type' => 'container', + ]; + $form['country']['country_filter_id']['error'] = [ + '#type' => 'markup', + '#markup' => t('You must add a filter for the country code to use this filter for administrative areas.'), + ]; + } + $form['country']['country_filter_id']['#states'] = [ + 'visible' => [ + ':input[name="options[country][country_source]"]' => ['value' => 'filter'], + ], + ]; + + $countries = $this->getAdministrativeAreaCountries(); + + $form['country']['country_static_code'] = [ + '#type' => 'select', + '#title' => t('Predefined country for administrative areas'), + '#options' => $countries, + '#empty_value' => '', + '#default_value' => $this->options['country']['country_static_code'], + '#ajax' => [ + 'callback' => [get_class($this), 'ajaxRefreshCountry'], + 'wrapper' => 'admin-area-value-options-ajax-wrapper', + ], + '#states' => [ + 'visible' => [ + ':input[name="options[country][country_source]"]' => ['value' => 'static'], + ], + ], + ]; + + // @todo This should appear directly above $form['expose']['label']. + $form['expose']['label_type'] = [ + '#type' => 'radios', + '#title' => $this->t('Label type'), + '#options' => [ + 'static' => $this->t('Static'), + 'dynamic' => $this->t('Dynamic (an appropriate label will be set based on the active country)'), + ], + '#default_value' => $this->options['expose']['label_type'], + '#states' => [ + 'visible' => [ + ':input[name="options[expose_button][checkbox][checkbox]"]' => ['checked' => TRUE], + ], + ], + ]; + + parent::buildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateOptionsForm(&$form, FormStateInterface $form_state) { + if (empty($form_state)) { + return; + } + $is_exposed = !empty($this->options['exposed']); + + $country_source = $form_state->getValue(['options', 'country', 'country_source']); + switch ($country_source) { + case 'argument': + $country_argument = $form_state->getValue(['options', 'country', 'country_argument_id']); + if (empty($country_argument)) { + $error = $this->t("The country contextual filter must be defined for this filter to work using 'contextual filter' for the 'Country source'."); + $form_state->setError($form['country']['country_source'], $error); + } + if (empty($is_exposed)) { + $error = $this->t('This filter must be exposed to use a contextual filter to specify the country.'); + $form_state->setError($form['country']['country_source'], $error); + } + break; + + case 'filter': + $country_filter = $form_state->getValue(['options', 'country', 'country_filter_id']); + if (empty($country_filter)) { + $error = $this->t("The country filter must be defined for this filter to work using 'exposed filter' for the 'Country source'."); + $form_state->setError($form['country']['country_source'], $error); + } + if (empty($is_exposed)) { + $error = $this->t('This filter must be exposed to use a filter to specify the country.'); + $form_state->setError($form['country']['country_source'], $error); + } + break; + + case 'static': + $country_code = $form_state->getValue(['options', 'country', 'country_static_code']); + if (empty($country_code)) { + $error = $this->t('The predefined country must be set for this filter to work.'); + $form_state->setError($form['country']['country_static_code'], $error); + } + break; + + default: + $error = $this->t('The source for the country must be defined for this filter to work.'); + $form_state->setError($form['country']['country_source'], $error); + + break; + } + + parent::validateOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function buildExposeForm(&$form, FormStateInterface $form_state) { + parent::buildExposeForm($form, $form_state); + // Only show the label element if we're configured for a static label. + $form['expose']['label']['#states'] = [ + 'visible' => [ + ':input[name="options[expose][label_type]"]' => ['value' => 'static'], + ], + ]; + // Only show the reduce option if we have a static country. If we're + // getting values from a filter or argument, there are no fixed values to + // reduce to. + $form['expose']['reduce']['#states'] = [ + 'visible' => [ + ':input[name="options[country][country_source]"]' => ['value' => 'static'], + ], + ]; + // Repair the wrapper container on $form['value'] clobbered by + // FilterPluginBase::buildExposeForm(). + $form['value']['#prefix'] = '
'; + $form['value']['#suffix'] = '
'; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitExposeForm($form, FormStateInterface $form_state) { + // If the country source is anything other than static, we have to + // ignore/disable the "reduce" option since it doesn't make any sense and + // will cause problems if the stale configuration is saved. + // Similarly, we clear out any selections for specific administrative areas. + $country_source = $form_state->getValue(['options', 'country', 'country_source']); + if ($country_source != 'static') { + $form_state->setValue(['options', 'expose', 'reduce'], FALSE); + $form_state->setValue(['options', 'value'], []); + } + parent::submitExposeForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function showValueForm(&$form, FormStateInterface $form_state) { + $this->valueForm($form, $form_state); + $form['value']['#prefix'] = '
'; + $form['value']['#suffix'] = '
'; + } + + /** + * {@inheritdoc} + */ + public function valueForm(&$form, FormStateInterface $form_state) { + $this->valueOptions = []; + $this->formState = $form_state; + + $country_source = $this->getCountrySource(); + + if ($country_source == 'static' || $form_state->get('exposed')) { + $this->getCurrentCountry(); + parent::valueForm($form, $form_state); + $form['value']['#after_build'][] = [get_class($this), 'clearValues']; + } + else { + $form['value'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'admin-area-value-options-ajax-wrapper'], + ]; + $form['value']['message'] = [ + '#type' => 'markup', + '#markup' => t("You can only select options here if you use a predefined country for the 'Country source'."), + ]; + } + } + + /** + * {@inheritdoc} + */ + protected function valueSubmit($form, FormStateInterface $form_state) { + $this->formState = $form_state; + $country_source = $this->getCountrySource(); + if ($country_source == 'static') { + // Only save the values if we've got a static country code. + parent::valueSubmit($form, $form_state); + } + } + + /** + * {@inheritdoc} + */ + public function exposedInfo() { + $info = parent::exposedInfo(); + if ($this->options['expose']['label_type'] == 'dynamic') { + $current_country = $this->getCurrentCountry(); + if (!empty($current_country)) { + $address_format = $this->addressFormatRepository->get($current_country); + $labels = LabelHelper::getFieldLabels($address_format); + if (!empty($labels['administrativeArea'])) { + $info['label'] = $labels['administrativeArea']; + } + } + } + return $info; + } + + /** + * Gets the current source for the country code. + * + * If defined in the current values of the configuration form, use + * that. Otherwise, fall back to the filter configuration. + * + * @return string + * The country source. + */ + protected function getCountrySource() { + // If we're rebuilding via AJAX, we want the country source from the form + // state, not the configuration. + $country_source = ''; + if (!empty($this->formState)) { + // First, see if there's a legitimate value in the form state. + $form_value_country_source = $this->formState->getValue(['options', 'country', 'country_source']); + if (!empty($form_value_country_source)) { + $country_source = $form_value_country_source; + } + else { + // At various stages of building/validating the form, we might have + // user input but not yet have the value saved into the form + // state. So, if we have a form state but still don't have a value, + // see if it is defined in the user input. + $input = $this->formState->getUserInput(); + if (!empty($input['options']['country']['country_source'])) { + $country_source = $input['options']['country']['country_source']; + } + } + } + // If we don't have a source via the form state, use our configuration. + if (empty($country_source)) { + $country_source = $this->options['country']['country_source']; + } + return $country_source; + } + + /** + * Gets the currently active country code. + * + * The country source determines where to look for the country code. It can + * either be predefined, in which case we simply return the current value of + * the static country code (via form values or configuration). We can look + * for the country via a Views argument, in which case we determine the + * current value of the argument. Or we can get the country from another + * exposed filter, in which case we look in the form values to find the + * current country code from the other filter. + * + * @return string + * The 2-letter country code. + */ + protected function getCurrentCountry() { + $this->currentCountryCode = ''; + switch ($this->getCountrySource()) { + case 'argument': + $country_argument = $this->view->display_handler->getHandler('argument', $this->options['country']['country_argument_id']); + if (!empty($country_argument)) { + $this->currentCountryCode = $country_argument->getValue(); + } + break; + + case 'filter': + $country_filter = $this->view->display_handler->getHandler('filter', $this->options['country']['country_filter_id']); + if (!empty($country_filter) && !empty($this->formState)) { + $input = $this->formState->getUserInput(); + $country_filter_identifier = $country_filter->options['expose']['identifier']; + if (!empty($input[$country_filter_identifier])) { + if (is_array($input[$country_filter_identifier])) { + // @todo Maybe the config validation should prevent multi-valued + // country filters. For now, we only provide administrative area + // options if a single country is selected. + if (count($input[$country_filter_identifier]) == 1) { + $this->currentCountryCode = array_shift($input[$country_filter_identifier]); + } + } + else { + $this->currentCountryCode = $input[$country_filter_identifier]; + } + } + } + break; + + case 'static': + if (!empty($this->formState)) { + // During filter configuration validation, we still need to know the + // current country code, but the values won't yet be saved into the + // ones accessible via FormStateInterface::getValue(). So, directly + // inspect the user input instead of the official form values. + $input = $this->formState->getUserInput(); + if (!empty($input['options']['country']['country_static_code'])) { + $form_input_country_code = $input['options']['country']['country_static_code']; + } + } + $this->currentCountryCode = !empty($form_input_country_code) ? $form_input_country_code : $this->options['country']['country_static_code']; + break; + + } + + // Since the country code can come from all sorts of non-validated user + // input (e.g. GET parameters) and since it might be 'All', ensure we've + // got a valid country code before we proceed. Other code in this + // filter (and especially upstream in the AddressFormatRepository and + // others) will explode if passed an invalid country code. + if (!empty($this->currentCountryCode)) { + $all_countries = $this->countryRepository->getList(); + if (empty($all_countries[$this->currentCountryCode])) { + $this->currentCountryCode = ''; + } + } + return $this->currentCountryCode; + } + + /** + * {@inheritdoc} + */ + public function getValueOptions() { + $this->valueOptions = []; + if (($country_code = $this->getCurrentCountry())) { + $parents[] = $country_code; + $locale = \Drupal::languageManager()->getConfigOverrideLanguage()->getId(); + $subdivisions = $this->subdivisionRepository->getList($parents, $locale); + $this->valueOptions = $subdivisions; + } + return $this->valueOptions; + } + + /** + * {@inheritdoc} + */ + public function buildExposedForm(&$form, FormStateInterface $form_state) { + parent::buildExposedForm($form, $form_state); + // Hide the form element if we have no options to select. + // (e.g. the country isn't set or it doesn't use administrative areas). + if (empty($this->valueOptions)) { + $identifier = $this->options['expose']['identifier']; + $form[$identifier]['#access'] = FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + switch ($this->options['country']['country_source']) { + case 'argument': + return $this->t('exposed: country set via contextual filter'); + + case 'filter': + return $this->t('exposed: country set via exposed filter'); + + case 'static': + if (!empty($this->options['exposed'])) { + return $this->t('exposed: fixed country: @country', ['@country' => $this->options['country']['country_static_code']]); + } + return $this->t('fixed country: @country', ['@country' => $this->options['country']['country_static_code']]); + } + return $this->t('broken configuration'); + } + + /** + * Gets a list of countries that have administrative areas. + * + * @param array $available_countries + * The available countries to filter by. + * Defaults to the available countries for this filter. + * + * @return array + * An array of country names, keyed by country code. + */ + public function getAdministrativeAreaCountries(array $available_countries = NULL) { + if (!isset($available_countries)) { + $available_countries = $this->getAvailableCountries(); + } + + $countries = []; + foreach ($available_countries as $country_code => $country_name) { + $address_format = $this->addressFormatRepository->get($country_code); + $subdivision_depth = $address_format->getSubdivisionDepth(); + if ($subdivision_depth > 0) { + $countries[$country_code] = $country_name; + } + } + + return $countries; + } + + /** + * Ajax callback. + */ + public static function ajaxRefreshCountry(array $form, FormStateInterface $form_state) { + return $form['options']['value']; + } + + /** + * Clears the administrative area form values when the country changes. + * + * Implemented as an #after_build callback because #after_build runs before + * validation, allowing the values to be cleared early enough to prevent the + * "Illegal choice" error. + */ + public static function clearValues(array $element, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + if (!$triggering_element) { + return $element; + } + + $triggering_element_name = end($triggering_element['#parents']); + if ($triggering_element_name == 'country_static_code' || $triggering_element_name == 'country_source') { + foreach ($element['#options'] as $key => $option) { + $element[$key]['#value'] = 0; + } + $element['#value'] = []; + + $input = &$form_state->getUserInput(); + $input['options']['value'] = []; + } + + return $element; + } + +} diff --git a/src/Plugin/views/filter/CountryAwareInOperatorBase.php b/src/Plugin/views/filter/CountryAwareInOperatorBase.php new file mode 100644 index 0000000000000000000000000000000000000000..2f16da82712d9bd67dfb36224ac37e8efb9b5e14 --- /dev/null +++ b/src/Plugin/views/filter/CountryAwareInOperatorBase.php @@ -0,0 +1,185 @@ +countryRepository = $country_repository; + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('address.country_repository'), + $container->get('entity_type.manager'), + $container->get('entity_field.manager') + ); + } + + /** + * Gets the name of the entity field on which this filter operates. + * + * @return string + * The field name. + */ + protected function getFieldName() { + if (isset($this->configuration['field_name'])) { + // Configurable field. + $field_name = $this->configuration['field_name']; + } + else { + // Base field. + $field_name = $this->configuration['entity field']; + } + + return $field_name; + } + + /** + * Gets the list of available countries for the current entity field. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type, defaults to the current type for this filter. + * @param string $field_name + * The field name, defaults to the current field name for this filter. + * + * @return array + * An array of available country codes, including the full list when unrestricted. + */ + protected function getAvailableCountries(EntityTypeInterface $entity_type = NULL, $field_name = NULL) { + if (!isset($entity_type)) { + $entity_type_id = $this->getEntityType(); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + } + if (!isset($field_name)) { + $field_name = $this->getFieldName(); + } + + $bundles = $this->getBundles($entity_type, $field_name); + $storage = $this->entityTypeManager->getStorage($entity_type->id()); + $countries_by_bundle = []; + foreach ($bundles as $bundle) { + $values = []; + if ($bundle_key = $entity_type->getKey('bundle')) { + $values[$bundle_key] = $bundle; + } + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $storage->create($values); + if ($entity->hasField($field_name)) { + $countries_by_bundle[$bundle] = $entity->get($field_name)->appendItem()->getAvailableCountries(); + } + } + // Create the unified list, valid across bundles. + // Start by filtering out lists that are empty cause no restrictions apply. + $countries = []; + $countries_by_bundle = array_filter($countries_by_bundle); + if (count($countries_by_bundle) === 1) { + $countries = reset($countries_by_bundle); + } + elseif (count($countries_by_bundle) > 1) { + // Leave only the country codes that are common to all lists. + $countries = array_pop($countries_by_bundle); + foreach ($countries_by_bundle as $list) { + $countries = array_intersect_key($countries, $list); + } + } + + $available_countries = $this->countryRepository->getList(); + if (!empty($countries)) { + $available_countries = array_intersect_key($available_countries, $countries); + } + + return $available_countries; + } + + /** + * Gets the bundles for the current entity field. + * + * If the view has a non-exposed bundle filter, the bundles are taken from + * there. Otherwise, the field's bundles are used. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The current entity type. + * @param string $field_name + * The current field name. + * + * @return string[] + * The bundles. + */ + protected function getBundles(EntityTypeInterface $entity_type, $field_name) { + $bundles = []; + $bundle_key = $entity_type->getKey('bundle'); + if ($bundle_key && isset($this->view->filter[$bundle_key])) { + $filter = $this->view->filter[$bundle_key]; + if (!$filter->isExposed() && !empty($filter->value)) { + // 'all' is added by Views and isn't a bundle. + $bundles = array_diff($filter->value, ['all']); + } + } + // Fallback to the list of bundles the field is attached to. + if (empty($bundles)) { + $map = $this->entityFieldManager->getFieldMap(); + $bundles = $map[$entity_type->id()][$field_name]['bundles']; + } + + return $bundles; + } + +} diff --git a/src/Plugin/views/filter/CountryCode.php b/src/Plugin/views/filter/CountryCode.php index 374b6198a8a02e22e1d8c33d47770a636e1fc9d3..aa2da72ebce77868619b8dbd7f6b2c090ae81103 100644 --- a/src/Plugin/views/filter/CountryCode.php +++ b/src/Plugin/views/filter/CountryCode.php @@ -2,13 +2,6 @@ namespace Drupal\address\Plugin\views\filter; -use CommerceGuys\Addressing\Country\CountryRepositoryInterface; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\views\Plugin\views\filter\InOperator; -use Symfony\Component\DependencyInjection\ContainerInterface; - /** * Filter by country. * @@ -16,181 +9,17 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * * @ViewsFilter("country_code") */ -class CountryCode extends InOperator { - - /** - * The country repository. - * - * @var \CommerceGuys\Addressing\Country\CountryRepositoryInterface - */ - protected $countryRepository; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * The entity field manager. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface - */ - protected $entityFieldManager; - - /** - * Constructs a new CountryCode object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \CommerceGuys\Addressing\Country\CountryRepositoryInterface $country_repository - * The country repository. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager - * The entity field manager. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, CountryRepositoryInterface $country_repository, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - - $this->countryRepository = $country_repository; - $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('address.country_repository'), - $container->get('entity_type.manager'), - $container->get('entity_field.manager') - ); - } +class CountryCode extends CountryAwareInOperatorBase { /** * {@inheritdoc} */ public function getValueOptions() { if (!isset($this->valueOptions)) { - $entity_type_id = $this->getEntityType(); - $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); - $field_name = $this->getFieldName(); - $available_countries = $this->getAvailableCountries($entity_type, $field_name); - $countries = $this->countryRepository->getList(); - if (!empty($available_countries)) { - $countries = array_intersect_key($countries, $available_countries); - } - - $this->valueOptions = $countries; + $this->valueOptions = $this->getAvailableCountries(); } return $this->valueOptions; } - /** - * Gets the name of the entity field on which this filter operates. - * - * @return string - * The field name. - */ - protected function getFieldName() { - if (isset($this->configuration['field_name'])) { - // Configurable field. - $field_name = $this->configuration['field_name']; - } - else { - // Base field. - $field_name = $this->configuration['entity field']; - } - - return $field_name; - } - - /** - * Gets the list of available countries for the current entity field. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The current entity type. - * @param string $field_name - * The current field name. - * - * @return array - * An array of country codes. Empty if the country list isn't restricted. - */ - protected function getAvailableCountries(EntityTypeInterface $entity_type, $field_name) { - $bundles = $this->getBundles($entity_type, $field_name); - $storage = $this->entityTypeManager->getStorage($entity_type->id()); - $countries_by_bundle = []; - foreach ($bundles as $bundle) { - $values = []; - if ($bundle_key = $entity_type->getKey('bundle')) { - $values[$bundle_key] = $bundle; - } - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - $entity = $storage->create($values); - if ($entity->hasField($field_name)) { - $countries_by_bundle[$bundle] = $entity->get($field_name)->appendItem()->getAvailableCountries(); - } - } - // Create the unified list, valid across bundles. - // Start by filtering out lists that are empty cause no restrictions apply. - $countries = []; - $countries_by_bundle = array_filter($countries_by_bundle); - if (count($countries_by_bundle) === 1) { - $countries = reset($countries_by_bundle); - } - elseif (count($countries_by_bundle) > 1) { - // Leave only the country codes that are common to all lists. - $countries = array_pop($countries_by_bundle); - foreach ($countries_by_bundle as $list) { - $countries = array_intersect_key($countries, $list); - } - } - - return $countries; - } - - /** - * Gets the bundles for the current entity field. - * - * If the view has a non-exposed bundle filter, the bundles are taken from - * there. Otherwise, the field's bundles are used. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The current entity type. - * @param string $field_name - * The current field name. - * - * @return string[] - * The bundles. - */ - protected function getBundles(EntityTypeInterface $entity_type, $field_name) { - $bundles = []; - $bundle_key = $entity_type->getKey('bundle'); - if ($bundle_key && isset($this->view->filter[$bundle_key])) { - $filter = $this->view->filter[$bundle_key]; - if (!$filter->isExposed() && !empty($filter->value)) { - // 'all' is added by Views and isn't a bundle. - $bundles = array_diff($filter->value, ['all']); - } - } - // Fallback to the list of bundles the field is attached to. - if (empty($bundles)) { - $map = $this->entityFieldManager->getFieldMap(); - $bundles = $map[$entity_type->id()][$field_name]['bundles']; - } - - return $bundles; - } - } diff --git a/tests/modules/address_test/address_test.info.yml b/tests/modules/address_test/address_test.info.yml index 283730a3e4055dd6b3ecb5e67f9cd179c134fcd2..60fed48dd51be7b99121098b672a6f817a5d3c6e 100644 --- a/tests/modules/address_test/address_test.info.yml +++ b/tests/modules/address_test/address_test.info.yml @@ -1,5 +1,8 @@ name: Address test type: module -description: 'Provides functionality for testing the events in address module.' +description: 'Provides functionality for testing the address module.' package: Testing core: 8.x +dependencies: + - address + - views diff --git a/tests/modules/address_test/config/install/field.field.node.address_test.field_address_test.yml b/tests/modules/address_test/config/install/field.field.node.address_test.field_address_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..bf09342952a848bb10bf17bd61d9ba0a16ed0758 --- /dev/null +++ b/tests/modules/address_test/config/install/field.field.node.address_test.field_address_test.yml @@ -0,0 +1,34 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_address_test + - node.type.address_test + module: + - address +id: node.address_test.field_address_test +field_name: field_address_test +entity_type: node +bundle: address_test +label: Address +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: + available_countries: { } + fields: + administrativeArea: administrativeArea + locality: locality + dependentLocality: dependentLocality + postalCode: postalCode + sortingCode: sortingCode + addressLine1: addressLine1 + addressLine2: addressLine2 + organization: organization + givenName: givenName + additionalName: additionalName + familyName: familyName + langcode_override: '' +field_type: address diff --git a/tests/modules/address_test/config/install/field.storage.node.field_address_test.yml b/tests/modules/address_test/config/install/field.storage.node.field_address_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..5c6699e062a40a44f31d5d2891b1644af17b055f --- /dev/null +++ b/tests/modules/address_test/config/install/field.storage.node.field_address_test.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - address + - node +id: node.field_address_test +field_name: field_address_test +entity_type: node +type: address +settings: { } +module: address +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/tests/modules/address_test/config/install/node.type.address_test.yml b/tests/modules/address_test/config/install/node.type.address_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..395d35dda6ae990bd25e58592a23ac71a3c96d2b --- /dev/null +++ b/tests/modules/address_test/config/install/node.type.address_test.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +name: 'Address Test' +type: address_test +description: '' +help: '' +new_revision: false +preview_mode: 1 +display_submitted: true diff --git a/tests/modules/address_test/config/optional/views.view.address_test_filter_administrative_area.yml b/tests/modules/address_test/config/optional/views.view.address_test_filter_administrative_area.yml new file mode 100644 index 0000000000000000000000000000000000000000..dc145d0ca3f921753c24f79295a2bc445caa74e6 --- /dev/null +++ b/tests/modules/address_test/config/optional/views.view.address_test_filter_administrative_area.yml @@ -0,0 +1,281 @@ +langcode: en +status: true +dependencies: + module: + - address + - node + - user +id: address_test_filter_administrative_area +label: 'address test filter administrative area' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + offset: 0 + style: + type: default + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + field_address_test_country_code: + id: field_address_test_country_code + table: node__field_address_test + field: field_address_test_country_code + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: field_address_test_country_code_op + label: Country + description: '' + use_operator: false + operator: field_address_test_country_code_op + identifier: field_address_test_country_code + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: country_code + field_address_test_administrative_area: + id: field_address_test_administrative_area + table: node__field_address_test + field: field_address_test_administrative_area + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: field_address_test_administrative_area_op + label: 'Administrative area (static label)' + description: '' + use_operator: false + operator: field_address_test_administrative_area_op + identifier: field_address_test_administrative_area + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + label_type: static + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + country: + country_source: static + country_argument_id: '' + country_filter_id: field_address_test_country_code + country_static_code: AR + plugin_id: administrative_area + sorts: { } + title: 'address views administrative area test' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + field_address_test_country_code: + id: field_address_test_country_code + table: node__field_address_test + field: field_address_test_country_code + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: false + plugin_id: string + display_extenders: { } + filter_groups: + operator: AND + groups: + 1: AND + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: 'address-test/views/filter-administrative-area' + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/tests/src/Functional/Views/AdministrativeAreaFilterTest.php b/tests/src/Functional/Views/AdministrativeAreaFilterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3f77386b42270be941f99404b9a416dc8c8c0808 --- /dev/null +++ b/tests/src/Functional/Views/AdministrativeAreaFilterTest.php @@ -0,0 +1,212 @@ +user = $this->drupalCreateUser(['access content']); + $this->drupalLogin($this->user); + } + + /** + * Test options for administrative area using a static country code. + */ + public function testStaticCountryAdministrativeAreaOptions() { + $view = Views::getView('address_test_filter_administrative_area'); + $filters = $view->getDisplay()->getOption('filters'); + $filters['field_address_test_administrative_area']['country']['country_source'] = 'static'; + $filters['field_address_test_administrative_area']['country']['country_static_code'] = 'BR'; + $view->getDisplay()->overrideOption('filters', $filters); + $view->save(); + + $this->drupalGet('address-test/views/filter-administrative-area'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldExists('field_address_test_administrative_area'); + $this->assertAdministrativeAreaOptions('BR'); + } + + /** + * Test options for administrative area using a contextual country filter. + */ + public function testContextualCountryFilterAdministrativeAreaOptions() { + $view = Views::getView('address_test_filter_administrative_area'); + $filters = $view->getDisplay()->getOption('filters'); + $filters['field_address_test_administrative_area']['country']['country_source'] = 'argument'; + $filters['field_address_test_administrative_area']['country']['country_argument_id'] = 'field_address_test_country_code'; + $view->getDisplay()->overrideOption('filters', $filters); + $view->save(); + + // With no country selected, the administrative area shouldn't exist. + $this->drupalGet('address-test/views/filter-administrative-area'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldNotExists('field_address_test_administrative_area'); + + // For a country without admin areas, the filter still shouldn't exist. + $this->drupalGet('address-test/views/filter-administrative-area/CR'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldNotExists('field_address_test_administrative_area'); + + // For countries with administrative areas, validate the options. + foreach (['BR', 'EG', 'MX', 'US'] as $country) { + $this->drupalGet("address-test/views/filter-administrative-area/$country"); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldExists('field_address_test_administrative_area'); + $this->assertAdministrativeAreaOptions($country); + } + } + + /** + * Test options for administrative area using an exposed country filter. + */ + public function testExposedCountryFilterAdministrativeAreaOptions() { + $view = Views::getView('address_test_filter_administrative_area'); + $filters = $view->getDisplay()->getOption('filters'); + $filters['field_address_test_administrative_area']['country']['country_source'] = 'filter'; + $filters['field_address_test_administrative_area']['country']['country_filter_id'] = 'field_address_test_country_code'; + $view->getDisplay()->overrideOption('filters', $filters); + $view->save(); + + // With no country selected, the administrative area shouldn't exist. + $this->drupalGet('address-test/views/filter-administrative-area'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldNotExists('field_address_test_administrative_area'); + + // For a country without admin areas, the filter still shouldn't exist. + $options = ['query' => ['field_address_test_country_code' => 'CR']]; + $this->drupalGet('address-test/views/filter-administrative-area', $options); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldNotExists('field_address_test_administrative_area'); + + // For countries with admin areas, validate the options. + foreach (['BR', 'EG', 'MX', 'US'] as $country) { + $options = ['query' => ['field_address_test_country_code' => $country]]; + $this->drupalGet('address-test/views/filter-administrative-area', $options); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->fieldExists('field_address_test_administrative_area'); + $this->assertAdministrativeAreaOptions($country); + } + } + + /** + * Test that the static vs. dynamic label feature works properly. + */ + public function testAdministrativeAreaLabels() { + $static_label = 'Administrative area (static label)'; + $dynamic_labels = [ + 'AE' => 'Emirate', + 'BR' => 'State', + 'CA' => 'Province', + 'US' => 'State', + ]; + + // Force the view into our expected configuration. Use contextual filter + // to set the country, and start with static labels. + $view = Views::getView('address_test_filter_administrative_area'); + $filters = $view->getDisplay()->getOption('filters'); + $filters['field_address_test_administrative_area']['country']['country_source'] = 'argument'; + $filters['field_address_test_administrative_area']['country']['country_argument_id'] = 'field_address_test_country_code'; + $filters['field_address_test_administrative_area']['expose']['label_type'] = 'static'; + $filters['field_address_test_administrative_area']['expose']['label'] = $static_label; + $view->getDisplay()->overrideOption('filters', $filters); + $view->save(); + + foreach ($dynamic_labels as $country => $dynamic_label) { + $this->drupalGet("address-test/views/filter-administrative-area/$country"); + $this->assertSession()->pageTextContains($static_label); + $this->assertSession()->pageTextNotContains($dynamic_label); + } + + // Configure for dynamic labels and test again. + $view = Views::getView('address_test_filter_administrative_area'); + $filters['field_address_test_administrative_area']['expose']['label_type'] = 'dynamic'; + $view->getDisplay()->overrideOption('filters', $filters); + $view->save(); + + foreach ($dynamic_labels as $country => $dynamic_label) { + $this->drupalGet("address-test/views/filter-administrative-area/$country"); + $this->assertSession()->pageTextNotContains($static_label); + $this->assertSession()->pageTextContains($dynamic_label); + } + } + + /** + * Assert the right administrative area options for a given country code. + * + * @param string $active_country + * The country code. + */ + protected function assertAdministrativeAreaOptions($active_country) { + // These are not exhaustive lists, nor are the keys guaranteed to be unique. + $areas = [ + 'BR' => [ + 'AM' => 'Amazonas', + 'BA' => 'Bahia', + 'PE' => 'Pernambuco', + 'RJ' => 'Rio de Janeiro', + ], + 'EG' => [ + 'Alexandria Governorate' => 'Alexandria Governorate', + 'Cairo Governorate' => 'Cairo Governorate', + ], + 'MX' => [ + 'CHIS' => 'Chiapas', + 'JAL' => 'Jalisco', + 'OAX' => 'Oaxaca', + 'VER' => 'Veracruz', + ], + 'US' => [ + 'LA' => 'Louisiana', + 'MA' => 'Massachusetts', + 'WI' => 'Wisconsin', + ], + ]; + foreach ($areas as $country => $areas) { + foreach ($areas as $area_key => $area_value) { + // For the active country, ensure both the key and value match. + if ($country == $active_country) { + $this->assertSession()->optionExists('edit-field-address-test-administrative-area', $area_key); + $this->assertSession()->optionExists('edit-field-address-test-administrative-area', $area_value); + } + // Otherwise, we can't assume the keys are unique (e.g. 'MA' is a + // state code in many different countries), so all we can safely + // assume is that the state value strings aren't on the page. + else { + $this->assertSession()->pageTextNotContains($area_value); + } + } + } + } + +}