diff --git a/config/schema/search_api.datasource.schema.yml b/config/schema/search_api.datasource.schema.yml index c607af8..18ae3cb 100644 --- a/config/schema/search_api.datasource.schema.yml +++ b/config/schema/search_api.datasource.schema.yml @@ -11,3 +11,9 @@ sequence: type: string label: 'An entity bundle' + languages: + type: sequence + label: 'The selected languages' + sequence: + type: string + label: 'An entity language' diff --git a/src/Plugin/search_api/datasource/ContentEntity.php b/src/Plugin/search_api/datasource/ContentEntity.php index b292c51..113ad3c 100644 --- a/src/Plugin/search_api/datasource/ContentEntity.php +++ b/src/Plugin/search_api/datasource/ContentEntity.php @@ -14,6 +14,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\Plugin\DataType\EntityAdapter; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\TypedDataManager; @@ -56,6 +57,13 @@ class ContentEntity extends DatasourcePluginBase { protected $configFactory; /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, array $plugin_definition) { @@ -91,6 +99,10 @@ class ContentEntity extends DatasourcePluginBase { $config_factory = $container->get('config.factory'); $datasource->setConfigFactory($config_factory); + /** @var $language_manager \Drupal\Core\Language\LanguageManagerInterface */ + $language_manager = $container->get('language_manager'); + $datasource->setLanguageManager($language_manager); + return $datasource; } @@ -195,6 +207,27 @@ class ContentEntity extends DatasourcePluginBase { $this->configFactory = $config_factory; return $this; } + + /** + * Sets the language manager. + * + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The new language manager. + */ + public function setLanguageManager(LanguageManagerInterface $language_manager) { + $this->languageManager = $language_manager; + } + + /** + * Retrieves the language manager. + * + * @return \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function getLanguageManager() { + return $this->languageManager ?: \Drupal::languageManager(); + } + /** * {@inheritdoc} */ @@ -227,6 +260,24 @@ class ContentEntity extends DatasourcePluginBase { $entity_ids[$entity_id][$item_id] = $langcode; } + $allowed_languages = array(); + $all_languages = $this->getLanguageManager()->getLanguages(); + $configuration = $this->configuration; + + if ($configuration['default']) { + $allowed_languages = $all_languages; + foreach ($configuration['languages'] as $langcode) { + unset($allowed_languages[$langcode]); + } + } + else { + foreach ($configuration['languages'] as $langcode => $checked) { + if ($checked === $langcode) { + $allowed_languages[$langcode] = $all_languages[$langcode]; + } + } + } + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */ $entities = $this->getEntityStorage()->loadMultiple(array_keys($entity_ids)); $missing = array(); @@ -236,7 +287,8 @@ class ContentEntity extends DatasourcePluginBase { // @todo Also refuse to load entities from not-included bundles? This // would help to avoid possible race conditions when removing bundles // from the datasource config. See #2574583. - if (!empty($entities[$entity_id]) && $entities[$entity_id]->hasTranslation($langcode)) { + $indexed_translations = array_intersect_key($entities[$entity_id]->getTranslationLanguages(), $allowed_languages); + if (!empty($entities[$entity_id]) && $entities[$entity_id]->hasTranslation($langcode) && array_key_exists($langcode, $indexed_translations)) { $items[$item_id] = $entities[$entity_id]->getTranslation($langcode)->getTypedData(); } else { @@ -335,21 +387,27 @@ class ContentEntity extends DatasourcePluginBase { * {@inheritdoc} */ public function defaultConfiguration() { + $default_configuration = array(); + if ($this->hasBundles() || $this->isTranslatable()) { + $default_configuration['default'] = 1; + } + if ($this->hasBundles()) { - return array( - 'default' => 1, - 'bundles' => array(), - ); + $default_configuration['bundles'] = array(); } - return array(); + + if ($this->isTranslatable()) { + $default_configuration['languages'] = array(); + } + + return $default_configuration; } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - if ($this->hasBundles()) { - $bundles = $this->getEntityBundleOptions(); + if ($this->hasBundles() || $this->isTranslatable()) { $form['default'] = array( '#type' => 'radios', '#title' => $this->t('What should be indexed?'), @@ -359,6 +417,10 @@ class ContentEntity extends DatasourcePluginBase { ), '#default_value' => $this->configuration['default'], ); + } + + if ($this->hasBundles()) { + $bundles = $this->getEntityBundleOptions(); $form['bundles'] = array( '#type' => 'checkboxes', '#title' => $this->t('Bundles'), @@ -369,6 +431,16 @@ class ContentEntity extends DatasourcePluginBase { ); } + if ($this->isTranslatable()) { + $form['languages'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Languages'), + '#options' => $this->getTranslationOptions(), + '#default_value' => $this->configuration['languages'], + '#multiple' => TRUE, + ); + } + return $form; } @@ -390,6 +462,21 @@ class ContentEntity extends DatasourcePluginBase { } /** + * Retrieves the available languages of this entity type as an options list. + * + * @return array + * An associative array of language labels, keyed by the language name. + */ + protected function getTranslationOptions() { + $options = array(); + foreach ($this->getLanguageManager()->getLanguages() as $language) { + $options[$language->getId()] = $language->getName(); + } + + return $options; + } + + /** * {@inheritdoc} */ public function getItemId(ComplexDataInterface $item) { @@ -442,15 +529,29 @@ class ContentEntity extends DatasourcePluginBase { */ public function getDescription() { $summary = ''; + + // Add bundle information in the description. if ($this->hasBundles()) { $bundles = array_values(array_intersect_key($this->getEntityBundleOptions(), array_filter($this->configuration['bundles']))); if ($this->configuration['default'] == TRUE) { - $summary = $this->t('Excluded bundles: @bundles', array('@bundles' => implode(', ', $bundles))); + $summary .= $this->t('Excluded bundles: @bundles', array('@bundles' => implode(', ', $bundles))); + } + else { + $summary .= $this->t('Included bundles: @bundles', array('@bundles' => implode(', ', $bundles))); + } + } + + // Add language information in the description. + if ($this->isTranslatable()) { + $languages = array_values(array_intersect_key($this->getLanguageManager()->getLanguages(), array_filter($this->configuration['languages']))); + if ($this->configuration['default'] == TRUE) { + $summary .= $this->t('Excluded languages: @languages', array('@languages' => implode(', ', $languages))); } else { - $summary = $this->t('Included bundles: @bundles', array('@bundles' => implode(', ', $bundles))); + $summary .= $this->t('Included languages: @languages', array('@languages' => implode(', ', $languages))); } } + return $summary; } @@ -473,6 +574,16 @@ class ContentEntity extends DatasourcePluginBase { } /** + * Determines whether the entity type supports translations. + * + * @return bool + * TRUE if the entity is translatable, FALSE otherwise. + */ + protected function isTranslatable() { + return $this->getEntityType()->isTranslatable(); + } + + /** * Retrieves all bundles of this datasource's entity type. * * @return array @@ -528,6 +639,7 @@ class ContentEntity extends DatasourcePluginBase { $item_ids[] = "$entity_id:$langcode"; } } + return $item_ids; } diff --git a/src/Tests/IndexIntegrationTest.php b/src/Tests/IndexIntegrationTest.php new file mode 100644 index 0000000..056a826 --- /dev/null +++ b/src/Tests/IndexIntegrationTest.php @@ -0,0 +1,145 @@ +drupalLogin($this->adminUser); + + // Add extra languages. + ConfigurableLanguage::createFromLangcode('nl')->save(); + ConfigurableLanguage::createFromLangcode('xx-lolspeak')->save(); + + // Create a test server and index. + $this->getTestServer(); + $index = $this->getTestIndex(); + $this->indexId = $index->id(); + + // Create 2 articles. + $article1 = $this->drupalCreateNode(array('type' => 'article')); + $article2 = $this->drupalCreateNode(array('type' => 'article')); + + // Those 2 new nodes should be added to the tracking table immediately. + $tracked_items = $this->countTrackedItems(); + $this->assertEqual($tracked_items, 2, 'Two items are tracked.'); + + // Add translations. + $article1->addTranslation('nl', array('title' => 'test NL', 'body' => 'NL body')) + ->save(); + $article2->addTranslation('nl', array('title' => 'test2 NL', 'body' => 'NL body2')) + ->save(); + $article1->addTranslation('xx-lolspeak', array('title' => 'cats', 'body' => 'Cats test')) + ->save(); + + // The translations should be tracked as well, so we have a total of 5 + // indexed items. + $tracked_items = $this->countTrackedItems(); + $this->assertEqual($tracked_items, 5, 'Five items are tracked.'); + + // Clear index. + $this->drupalGet($this->getIndexPath()); + $this->drupalPostForm(NULL, array(), $this->t('Clear all indexed data')); + $this->drupalPostForm(NULL, array(), $this->t('Confirm')); + + // Make sure all 5 items are successfully indexed. + $this->drupalGet($this->getIndexPath()); + $this->drupalPostForm(NULL, array(), $this->t('Index now')); + $this->assertText($this->t('Successfully indexed 5 items')); + + // Change the datasource to disallow indexing of dutch. + $form_values = array('datasource_configs[entity:node][languages][nl]' => 1); + $this->drupalGet($this->getIndexPath('edit')); + $this->drupalPostForm(NULL, $form_values, $this->t('Save')); + $this->assertResponse(200); + $this->assertText('The index was successfully saved.'); + + // Make sure that we only have 3 indexed items now. The 2 original nodes + // + 1 translation in lolspeak, the 2 dutch translations should be ignored. + $this->drupalGet($this->getIndexPath()); + $this->drupalPostForm(NULL, array(), $this->t('Index now')); + $this->assertText($this->t('Successfully indexed 3 items')); + + // Change the datasource to only allow indexing of dutch. + $form_values = array( + 'datasource_configs[entity:node][default]' => 0, + 'datasource_configs[entity:node][bundles][article]' => 1, + 'datasource_configs[entity:node][languages][nl]' => 1, + ); + $this->drupalGet($this->getIndexPath('edit')); + $this->drupalPostForm(NULL, $form_values, $this->t('Save')); + $this->assertResponse(200); + $this->assertText('The index was successfully saved.'); + + // Make sure that we only have 2 index items. The only indexed items should + // be the dutch translations. + $this->drupalGet($this->getIndexPath()); + $this->drupalPostForm(NULL, array(), $this->t('Index now')); + $this->assertText($this->t('Successfully indexed 2 items')); + } + + /** + * Counts the number of tracked items in the test index. + * + * @return int + * The number of tracked items in the test index. + */ + protected function countTrackedItems() { + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load($this->indexId); + return $index->getTracker()->getTotalItemsCount(); + } + + /** + * Returns the system path for the test index. + * + * @param string|null $tab + * (optional) If set, the path suffix for a specific index tab. + * + * @return string + * A system path. + */ + protected function getIndexPath($tab = NULL) { + $path = 'admin/config/search/search-api/index/' . $this->indexId; + if ($tab) { + $path .= "/$tab"; + } + return $path; + } + +}