diff --git a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php index 8b34797..4fa54f3 100644 --- a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php +++ b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php @@ -2,10 +2,13 @@ namespace Drupal\content_translation; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\migrate\Event\MigrateEvents; +use Drupal\migrate\Event\MigrateImportEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -75,10 +78,26 @@ public function onConfigImporterImport() { } /** + * Listener for migration imports. + */ + public function onMigrateImport(MigrateImportEvent $event) { + $migration = $event->getMigration(); + $configuration = $migration->getDestinationConfiguration(); + $entity_types = NestedArray::getValue($configuration, ['content_translation_update_definitions']); + if ($entity_types) { + $entity_types = array_intersect_key($this->entityManager->getDefinitions(), array_flip($entity_types)); + $this->updateDefinitions($entity_types); + } + } + + /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events[ConfigEvents::IMPORT][] = ['onConfigImporterImport', 60]; + if (class_exists('\Drupal\migrate\Event\MigrateEvents')) { + $events[MigrateEvents::POST_IMPORT][] = ['onMigrateImport']; + } return $events; } diff --git a/core/modules/language/migration_templates/d6_language_content_settings.yml b/core/modules/language/migration_templates/d6_language_content_settings.yml index e5dc750..3bf9078 100644 --- a/core/modules/language/migration_templates/d6_language_content_settings.yml +++ b/core/modules/language/migration_templates/d6_language_content_settings.yml @@ -1,5 +1,6 @@ id: d6_language_content_settings label: Drupal 6 language content settings + migration_tags: - Drupal 6 source: @@ -39,6 +40,8 @@ process: 2: true destination: plugin: entity:language_content_settings + content_translation_update_definitions: + - node migration_dependencies: required: - d6_node_type diff --git a/core/modules/language/migration_templates/d7_language_content_settings.yml b/core/modules/language/migration_templates/d7_language_content_settings.yml index cbe935a..09437fa 100644 --- a/core/modules/language/migration_templates/d7_language_content_settings.yml +++ b/core/modules/language/migration_templates/d7_language_content_settings.yml @@ -39,6 +39,8 @@ process: 2: true destination: plugin: entity:language_content_settings + content_translation_update_definitions: + - node migration_dependencies: required: - d7_node_type diff --git a/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php b/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php index b4cf30b..18a95e6 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php @@ -94,8 +94,10 @@ public function supportsRollback() { * * @param array $id_map * The map row data for the item. + * @param int $update_action + * The rollback action to take if we are updating an existing item. */ - protected function setRollbackAction(array $id_map) { + protected function setRollbackAction(array $id_map, $update_action = MigrateIdMapInterface::ROLLBACK_PRESERVE) { // If the entity we're updating was previously migrated by us, preserve the // existing rollback action. if (isset($id_map['sourceid1'])) { @@ -104,7 +106,7 @@ protected function setRollbackAction(array $id_map) { // Otherwise, we're updating an entity which already existed on the // destination and want to make sure we do not delete it on rollback. else { - $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_PRESERVE; + $this->rollbackAction = $update_action; } } diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php index abf2dc9..1b2c2e7 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php @@ -124,7 +124,8 @@ public function fields(MigrationInterface $migration = NULL) { protected function getEntity(Row $row, array $old_destination_id_values) { $entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row); if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) { - $this->updateEntity($entity, $row); + // Allow updateEntity() to change the entity. + $entity = $this->updateEntity($entity, $row) ?: $entity; } else { // Attempt to ensure we always have a bundle. diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index 6a2fb98..3617f38 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\MigrateException; @@ -85,7 +86,12 @@ public function import(Row $row, array $old_destination_id_values = array()) { if (!$entity) { throw new MigrateException('Unable to get entity'); } - return $this->save($entity, $old_destination_id_values); + + $ids = $this->save($entity, $old_destination_id_values); + if (!empty($this->configuration['translations'])) { + $ids[] = $entity->language()->getId(); + } + return $ids; } /** @@ -105,11 +111,31 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i } /** + * Get whether this destination is for translations. + * + * @return bool + * Whether this destination is for translations. + */ + protected function isTranslationDestination() { + return !empty($this->configuration['translations']); + } + + /** * {@inheritdoc} */ public function getIds() { $id_key = $this->getKey('id'); $ids[$id_key]['type'] = 'integer'; + + if ($this->isTranslationDestination()) { + if ($key = $this->getKey('langcode')) { + $ids[$key]['type'] = 'string'; + } + else { + throw new MigrateException('This entity type does not support translation.'); + } + } + return $ids; } @@ -120,8 +146,29 @@ public function getIds() { * The entity to update. * @param \Drupal\migrate\Row $row * The row object to update from. + * + * @return NULL|\Drupal\Core\Entity\EntityInterface + * An updated entity, or NULL if it's the same as the one passed in. */ protected function updateEntity(EntityInterface $entity, Row $row) { + // By default, an update will be preserved. + $rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE; + + // Make sure we have the right translation. + if ($this->isTranslationDestination()) { + $property = $this->storage->getEntityType()->getKey('langcode'); + if ($row->hasDestinationProperty($property)) { + $language = $row->getDestinationProperty($property); + if (!$entity->hasTranslation($language)) { + $entity->addTranslation($language); + + // We're adding a translation, so delete it on rollback. + $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE; + } + $entity = $entity->getTranslation($language); + } + } + // If the migration has specified a list of properties to be overwritten, // clone the row with an empty set of destination values, and re-add only // the specified properties. @@ -140,7 +187,10 @@ protected function updateEntity(EntityInterface $entity, Row $row) { } } - $this->setRollbackAction($row->getIdMap()); + $this->setRollbackAction($row->getIdMap(), $rollback_action); + + // We might have a different (translated) entity, so return it. + return $entity; } /** @@ -185,4 +235,32 @@ protected function processStubRow(Row $row) { } } + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + if ($this->isTranslationDestination()) { + // Attempt to remove the translation. + $entity = $this->storage->load(reset($destination_identifier)); + if ($entity && $entity instanceof TranslatableInterface) { + if ($key = $this->getKey('langcode')) { + if (isset($destination_identifier[$key])) { + $langcode = $destination_identifier[$key]; + if ($entity->hasTranslation($langcode)) { + // Make sure we don't remove the default translation. + $translation = $entity->getTranslation($langcode); + if (!$translation->isDefaultTranslation()) { + $entity->removeTranslation($langcode); + $entity->save(); + } + } + } + } + } + } + else { + parent::rollback($destination_identifier); + } + } + } diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrate_external_translated_test.info.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrate_external_translated_test.info.yml new file mode 100644 index 0000000..065d512 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrate_external_translated_test.info.yml @@ -0,0 +1,8 @@ +name: 'Migration external translated test' +type: module +package: Testing +version: VERSION +core: 8.x +dependencies: + - node + - migrate diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml new file mode 100644 index 0000000..f643b60 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml @@ -0,0 +1,19 @@ +id: external_translated_test_node +label: External translated content +source: + plugin: migrate_external_translated_test + default_lang: true + constants: + type: external_test +process: + type: constants/type + title: title + langcode: + plugin: static_map + source: lang + map: + English: en + French: fr + Spanish: es +destination: + plugin: entity:node diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml new file mode 100644 index 0000000..ff29084 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml @@ -0,0 +1,27 @@ +id: external_translated_test_node_translation +label: External translated content translations +source: + plugin: migrate_external_translated_test + default_lang: false + constants: + type: external_test +process: + nid: + plugin: migration + source: name + migration: external_translated_test_node + type: constants/type + title: title + langcode: + plugin: static_map + source: lang + map: + English: en + French: fr + Spanish: es +destination: + plugin: entity:node + translations: true +migration_dependencies: + required: + - external_translated_test_node diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/src/Plugin/migrate/source/MigrateExternalTranslatedTestSource.php b/core/modules/migrate/tests/modules/migrate_external_translated_test/src/Plugin/migrate/source/MigrateExternalTranslatedTestSource.php new file mode 100644 index 0000000..ceda6d8 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/src/Plugin/migrate/source/MigrateExternalTranslatedTestSource.php @@ -0,0 +1,77 @@ + 'cat', 'title' => 'Cat', 'lang' => 'English'], + ['name' => 'cat', 'title' => 'Chat', 'lang' => 'French'], + ['name' => 'cat', 'title' => 'Gato', 'lang' => 'Spanish'], + ['name' => 'dog', 'title' => 'Dog', 'lang' => 'English'], + ['name' => 'dog', 'title' => 'Chien', 'lang' => 'French'], + ['name' => 'monkey', 'title' => 'Monkey', 'lang' => 'English'], + ]; + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'name' => $this->t('Unique name'), + 'title' => $this->t('Title'), + 'lang' => $this->t('Language'), + ]; + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['name']['type'] = 'string'; + if (!$this->configuration['default_lang']) { + $ids['lang']['type'] = 'string'; + } + return $ids; + } + + /** + * {@inheritdoc} + */ + protected function initializeIterator() { + $data = []; + + // Keep the rows with the right languages. + $want_default = $this->configuration['default_lang']; + foreach ($this->import as $row) { + $is_english = $row['lang'] == 'English'; + if ($want_default == $is_english) { + $data[] = $row; + } + } + + return new \ArrayIterator($data); + } + +} diff --git a/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php b/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php new file mode 100644 index 0000000..352b660 --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php @@ -0,0 +1,159 @@ +installEntitySchema('entity_test_mul'); + + ConfigurableLanguage::createFromLangcode('en')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + + $this->storage = $this->container->get('entity.manager')->getStorage('entity_test_mul'); + } + + /** + * Check the existing translations of an entity. + * + * @param int $id + * The entity ID. + * @param string $default + * The expected default translation language code. + * @param string[] $others + * The expected other translation language codes. + */ + protected function assertTranslations($id, $default, $others = []) { + $entity = $this->storage->load($id); + $this->assertTrue($entity, "Entity exists"); + $this->assertEquals($default, $entity->language()->getId(), "Entity default translation"); + $translations = array_keys($entity->getTranslationLanguages(FALSE)); + sort($others); + sort($translations); + $this->assertEquals($others, $translations, "Entity translations"); + } + + /** + * Create the destination plugin to test. + * + * @param array $configuration + * The plugin configuration. + */ + protected function createDestination(array $configuration) { + $this->destination = new EntityContentBase( + $configuration, + 'fake_plugin_id', + [], + $this->getMock(MigrationInterface::class), + $this->storage, + [], + $this->container->get('entity.manager'), + $this->container->get('plugin.manager.field.field_type') + ); + } + + /** + * Test importing and rolling back translated entities. + */ + public function testTranslated() { + // Create a destination. + $this->createDestination(['translations' => TRUE]); + + // Create some pre-existing entities. + $this->storage->create(['id' => 1, 'langcode' => 'en'])->save(); + $this->storage->create(['id' => 2, 'langcode' => 'fr'])->save(); + $translated = $this->storage->create(['id' => 3, 'langcode' => 'en']); + $translated->save(); + $translated->addTranslation('fr')->save(); + + // Pre-assert that things are as expected. + $this->assertTranslations(1, 'en'); + $this->assertTranslations(2, 'fr'); + $this->assertTranslations(3, 'en', ['fr']); + $this->assertFalse($this->storage->load(4)); + + $destination_rows = [ + // Existing default translation. + ['id' => 1, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE], + // New translation. + ['id' => 2, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE], + // Existing non-default translation. + ['id' => 3, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE], + // Brand new row. + ['id' => 4, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE], + ]; + $rollback_actions = []; + + // Import some rows. + foreach ($destination_rows as $idx => $destination_row) { + $row = new Row([], []); + foreach ($destination_row as $key => $value) { + $row->setDestinationProperty($key, $value); + } + $this->destination->import($row); + + // Check that the rollback action is correct, and save it. + $this->assertEquals($destination_row['action'], $this->destination->rollbackAction()); + $rollback_actions[$idx] = $this->destination->rollbackAction(); + } + + $this->assertTranslations(1, 'en'); + $this->assertTranslations(2, 'fr', ['en']); + $this->assertTranslations(3, 'en', ['fr']); + $this->assertTranslations(4, 'fr'); + + // Rollback the rows. + foreach ($destination_rows as $idx => $destination_row) { + if ($rollback_actions[$idx] == MigrateIdMapInterface::ROLLBACK_DELETE) { + $this->destination->rollback($destination_row); + } + } + + // No change, update of existing translation. + $this->assertTranslations(1, 'en'); + // Remove added translation. + $this->assertTranslations(2, 'fr'); + // No change, update of existing translation. + $this->assertTranslations(3, 'en', ['fr']); + // No change, can't remove default translation. + $this->assertTranslations(4, 'fr'); + } + +} diff --git a/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php b/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php new file mode 100644 index 0000000..ce9c013 --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php @@ -0,0 +1,92 @@ +installSchema('system', ['sequences']); + $this->installSchema('node', array('node_access')); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + + // Create some languages. + ConfigurableLanguage::createFromLangcode('en')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + ConfigurableLanguage::createFromLangcode('es')->save(); + + // Create a content type. + NodeType::create([ + 'type' => 'external_test', + 'name' => 'Test node type', + ])->save(); + } + + /** + * Test importing and rolling back our data. + */ + public function testMigrations() { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->container->get('entity.manager')->getStorage('node'); + $this->assertEquals(0, count($storage->loadMultiple())); + + // Run the migrations. + $migration_ids = ['external_translated_test_node', 'external_translated_test_node_translation']; + $this->executeMigrations($migration_ids); + $this->assertEquals(3, count($storage->loadMultiple())); + + $node = $storage->load(1); + $this->assertEquals('en', $node->language()->getId()); + $this->assertEquals('Cat', $node->title->value); + $this->assertEquals('Chat', $node->getTranslation('fr')->title->value); + $this->assertEquals('Gato', $node->getTranslation('es')->title->value); + + $node = $storage->load(2); + $this->assertEquals('en', $node->language()->getId()); + $this->assertEquals('Dog', $node->title->value); + $this->assertEquals('Chien', $node->getTranslation('fr')->title->value); + $this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 2"); + + $node = $storage->load(3); + $this->assertEquals('en', $node->language()->getId()); + $this->assertEquals('Monkey', $node->title->value); + $this->assertFalse($node->hasTranslation('fr'), "No french translation for node 3"); + $this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 3"); + + $this->assertNull($storage->load(4), "No node 4 migrated"); + + // Roll back the migrations. + foreach ($migration_ids as $migration_id) { + $migration = $this->getMigration($migration_id); + $executable = new MigrateExecutable($migration, $this); + $executable->rollback(); + } + + $this->assertEquals(0, count($storage->loadMultiple())); + } + +} diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php index da20b49..33aec58 100644 --- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; @@ -97,6 +98,33 @@ public function testImportEntityLoadFailure() { $destination->import(new Row([], [])); } + /** + * Test that translation destination fails for untranslatable entities. + * + * @expectedException \Drupal\migrate\MigrateException + * @expectedExceptionMessage This entity type does not support translation + */ + public function testUntranslatable() { + // An entity type without a language. + $entity_type = $this->prophesize(ContentEntityType::class); + $entity_type->getKey('langcode')->willReturn(''); + $entity_type->getKey('id')->willReturn('id'); + + $this->storage->getEntityType()->willReturn($entity_type->reveal()); + + $destination = new EntityTestDestination( + [ 'translations' => TRUE ], + '', + [], + $this->migration->reveal(), + $this->storage->reveal(), + [], + $this->entityManager->reveal(), + $this->prophesize(FieldTypePluginManagerInterface::class)->reveal() + ); + $destination->getIds(); + } + } /** diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php index 97ed43e..bc39135 100644 --- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php +++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php @@ -8034,7 +8034,17 @@ ->values(array( 'uid' => '1', 'nid' => '9', - 'timestamp' => '1457655127', + 'timestamp' => '1468384961', +)) +->values(array( + 'uid' => '1', + 'nid' => '12', + 'timestamp' => '1468384823', +)) +->values(array( + 'uid' => '1', + 'nid' => '13', + 'timestamp' => '1468384931', )) ->execute(); @@ -34709,7 +34719,7 @@ 'access_callback' => 'user_access', 'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}', 'page_callback' => 'drupal_get_form', - 'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}', + 'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}', 'fit' => '15', 'number_parts' => '4', 'tab_parent' => '', @@ -34731,7 +34741,7 @@ 'access_callback' => 'user_access', 'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}', 'page_callback' => 'drupal_get_form', - 'page_arguments' => 'a:2:{i:0;s:24:"node_type_delete_confirm";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}', + 'page_arguments' => 'a:2:{i:0;s:24:"node_type_delete_confirm";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}', 'fit' => '31', 'number_parts' => '5', 'tab_parent' => '', @@ -34841,7 +34851,7 @@ 'access_callback' => 'user_access', 'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}', 'page_callback' => 'drupal_get_form', - 'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}', + 'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}', 'fit' => '31', 'number_parts' => '5', 'tab_parent' => 'admin/content/node-type/company', @@ -41334,6 +41344,40 @@ 'tnid' => '0', 'translate' => '0', )) +->values(array( + 'nid' => '10', + 'vid' => '13', + 'type' => 'page', + 'language' => 'en', + 'title' => 'The Real McCoy', + 'uid' => '1', + 'status' => '1', + 'created' => '1444238800', + 'changed' => '1444238808', + 'comment' => '2', + 'promote' => '1', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '10', + 'translate' => '0', +)) +->values(array( + 'nid' => '11', + 'vid' => '14', + 'type' => 'page', + 'language' => 'fr', + 'title' => 'Le Vrai McCoy', + 'uid' => '1', + 'status' => '1', + 'created' => '1444239050', + 'changed' => '1444239050', + 'comment' => '2', + 'promote' => '1', + 'moderate' => '0', + 'sticky' => '0', + 'tnid' => '10', + 'translate' => '0', +)) ->execute(); $connection->schema()->createTable('node_access', array( @@ -41388,25 +41432,6 @@ 'mysql_character_set' => 'utf8', )); -$connection->insert('node_access') -->fields(array( - 'nid', - 'gid', - 'realm', - 'grant_view', - 'grant_update', - 'grant_delete', -)) -->values(array( - 'nid' => '0', - 'gid' => '0', - 'realm' => 'all', - 'grant_view' => '1', - 'grant_update' => '0', - 'grant_delete' => '0', -)) -->execute(); - $connection->schema()->createTable('node_comment_statistics', array( 'fields' => array( 'nid' => array( @@ -41464,6 +41489,13 @@ 'comment_count', )) ->values(array( + 'nid' => '0', + 'last_comment_timestamp' => '1468384735', + 'last_comment_name' => NULL, + 'last_comment_uid' => '1', + 'comment_count' => '0', +)) +->values(array( 'nid' => '1', 'last_comment_timestamp' => '1388271197', 'last_comment_name' => NULL, @@ -41479,7 +41511,14 @@ )) ->values(array( 'nid' => '9', - 'last_comment_timestamp' => '1444671588', + 'last_comment_timestamp' => '1444238800', + 'last_comment_name' => NULL, + 'last_comment_uid' => '1', + 'comment_count' => '0', +)) +->values(array( + 'nid' => '10', + 'last_comment_timestamp' => '1444239050', 'last_comment_name' => NULL, 'last_comment_uid' => '1', 'comment_count' => '0', @@ -41727,6 +41766,28 @@ 'timestamp' => '1444671588', 'format' => '1', )) +->values(array( + 'nid' => '10', + 'vid' => '13', + 'uid' => '1', + 'title' => 'The Real McCoy', + 'body' => "In the original, Queen's English.", + 'teaser' => "In the original, Queen's English.", + 'log' => '', + 'timestamp' => '1444238808', + 'format' => '1', +)) +->values(array( + 'nid' => '11', + 'vid' => '14', + 'uid' => '1', + 'title' => 'Le Vrai McCoy', + 'body' => 'Ooh là là!', + 'teaser' => 'Ooh là là!', + 'log' => '', + 'timestamp' => '1444239050', + 'format' => '1', +)) ->execute(); $connection->schema()->createTable('node_type', array( @@ -41861,9 +41922,9 @@ 'title_label' => 'Name', 'has_body' => '1', 'body_label' => 'Description', - 'min_word_count' => '20', + 'min_word_count' => '0', 'custom' => '0', - 'modified' => '0', + 'modified' => '1', 'locked' => '0', 'orig_type' => 'company', )) @@ -44465,8 +44526,8 @@ 'signature' => '', 'signature_format' => '0', 'created' => '0', - 'access' => '1458198052', - 'login' => '1458193160', + 'access' => '1468384823', + 'login' => '1468384420', 'status' => '1', 'timezone' => NULL, 'language' => '', @@ -44810,12 +44871,16 @@ 'value' => 's:1:"2";', )) ->values(array( + 'name' => 'comment_company', + 'value' => 's:1:"2";', +)) +->values(array( 'name' => 'comment_controls_article', 'value' => 'i:3;', )) ->values(array( 'name' => 'comment_controls_company', - 'value' => 'i:3;', + 'value' => 's:1:"3";', )) ->values(array( 'name' => 'comment_controls_employee', @@ -44855,7 +44920,7 @@ )) ->values(array( 'name' => 'comment_default_mode_company', - 'value' => 'i:4;', + 'value' => 's:1:"4";', )) ->values(array( 'name' => 'comment_default_mode_employee', @@ -44895,7 +44960,7 @@ )) ->values(array( 'name' => 'comment_default_order_company', - 'value' => 'i:1;', + 'value' => 's:1:"1";', )) ->values(array( 'name' => 'comment_default_order_employee', @@ -44935,7 +45000,7 @@ )) ->values(array( 'name' => 'comment_default_per_page_company', - 'value' => 'i:50;', + 'value' => 's:2:"50";', )) ->values(array( 'name' => 'comment_default_per_page_employee', @@ -44975,7 +45040,7 @@ )) ->values(array( 'name' => 'comment_form_location_company', - 'value' => 'i:0;', + 'value' => 's:1:"0";', )) ->values(array( 'name' => 'comment_form_location_employee', @@ -45019,7 +45084,7 @@ )) ->values(array( 'name' => 'comment_preview_company', - 'value' => 'i:1;', + 'value' => 's:1:"1";', )) ->values(array( 'name' => 'comment_preview_employee', @@ -45063,7 +45128,7 @@ )) ->values(array( 'name' => 'comment_subject_field_company', - 'value' => 'i:1;', + 'value' => 's:1:"1";', )) ->values(array( 'name' => 'comment_subject_field_employee', @@ -45398,6 +45463,18 @@ 'value' => 's:22:"\S\H\O\R\T m/d/Y - H:i";', )) ->values(array( + 'name' => 'date_max_year', + 'value' => 'i:4000;', +)) +->values(array( + 'name' => 'date_min_year', + 'value' => 'i:1;', +)) +->values(array( + 'name' => 'date_php_min_year', + 'value' => 'i:1901;', +)) +->values(array( 'name' => 'dblog_row_limit', 'value' => 'i:10000;', )) @@ -45426,6 +45503,10 @@ 'value' => 's:5:"never";', )) ->values(array( + 'name' => 'event_nodeapi_company', + 'value' => 's:5:"never";', +)) +->values(array( 'name' => 'event_nodeapi_event', 'value' => 's:3:"all";', )) @@ -45487,7 +45568,11 @@ )) ->values(array( 'name' => 'form_build_id_article', - 'value' => 's:48:"form-mXZfFJxcCFGB80PPYtNOuwYbho6-xKTvrRLb3TAMkic";', + 'value' => 's:48:"form-t2zKJflpBD4rpYoGQH33ckjjWAYdo5lF3Hl1O_YnWyE";', +)) +->values(array( + 'name' => 'form_build_id_company', + 'value' => 's:48:"form-jFw2agRukPxjG5dG-N6joZLyoxXmCoxTzua0HUciqK0";', )) ->values(array( 'name' => 'forum_block_num_0', @@ -45518,6 +45603,14 @@ 'value' => 'a:2:{i:0;i:1;i:1;i:2;}', )) ->values(array( + 'name' => 'i18ntaxonomy_vocabulary', + 'value' => 'a:1:{i:4;s:1:"0";}', +)) +->values(array( + 'name' => 'i18n_lock_node_article', + 'value' => 'i:1;', +)) +->values(array( 'name' => 'image_jpeg_quality', 'value' => 'i:75;', )) @@ -45527,7 +45620,7 @@ )) ->values(array( 'name' => 'javascript_parsed', - 'value' => 'a:21:{i:0;s:14:"misc/jquery.js";i:1;s:14:"misc/drupal.js";i:2;s:19:"misc/tableheader.js";i:3;s:16:"misc/collapse.js";i:4;s:16:"misc/textarea.js";i:5;s:20:"modules/user/user.js";i:6;s:17:"misc/tabledrag.js";i:7;s:26:"modules/profile/profile.js";i:8;s:12:"misc/form.js";i:9;s:19:"misc/tableselect.js";i:10;s:20:"misc/autocomplete.js";s:10:"refresh:ga";s:7:"waiting";s:10:"refresh:ab";s:7:"waiting";s:10:"refresh:ca";s:7:"waiting";s:10:"refresh:fi";s:7:"waiting";s:10:"refresh:es";s:7:"waiting";i:11;s:16:"misc/progress.js";i:12;s:13:"misc/batch.js";s:10:"refresh:nl";s:7:"waiting";s:10:"refresh:de";s:7:"waiting";s:10:"refresh:pl";s:7:"waiting";}', + 'value' => 'a:28:{i:0;s:14:"misc/jquery.js";i:1;s:14:"misc/drupal.js";i:2;s:19:"misc/tableheader.js";i:3;s:16:"misc/collapse.js";i:4;s:16:"misc/textarea.js";i:5;s:20:"modules/user/user.js";i:6;s:17:"misc/tabledrag.js";i:7;s:26:"modules/profile/profile.js";i:8;s:12:"misc/form.js";i:9;s:19:"misc/tableselect.js";i:10;s:20:"misc/autocomplete.js";s:10:"refresh:ga";s:7:"waiting";s:10:"refresh:ab";s:7:"waiting";s:10:"refresh:ca";s:7:"waiting";s:10:"refresh:fi";s:7:"waiting";s:10:"refresh:es";s:7:"waiting";i:11;s:16:"misc/progress.js";i:12;s:13:"misc/batch.js";s:10:"refresh:nl";s:7:"waiting";s:10:"refresh:de";s:7:"waiting";s:10:"refresh:pl";s:7:"waiting";i:13;s:32:"sites/all/modules/cck/content.js";s:10:"refresh:fr";s:7:"waiting";s:10:"refresh:zu";s:7:"waiting";i:14;s:19:"misc/jquery.form.js";i:15;s:12:"misc/ahah.js";i:16;s:14:"misc/teaser.js";i:17;s:51:"sites/all/modules/i18n/i18ntaxonomy/i18ntaxonomy.js";}', )) ->values(array( 'name' => 'language_content_type_article', @@ -45538,10 +45631,6 @@ 'value' => 's:1:"2";', )) ->values(array( - 'name' => 'i18n_lock_node_article', - 'value' => 'i:1;', -)) -->values(array( 'name' => 'language_count', 'value' => 'i:11;', )) @@ -45590,6 +45679,10 @@ 'value' => 'a:1:{i:0;s:6:"status";}', )) ->values(array( + 'name' => 'node_options_company', + 'value' => 'a:2:{i:0;s:6:"status";i:1;s:7:"promote";}', +)) +->values(array( 'name' => 'node_options_forum', 'value' => 'a:1:{i:0;s:6:"status";}', )) @@ -45786,6 +45879,10 @@ 'value' => 'b:0;', )) ->values(array( + 'name' => 'upload_company', + 'value' => 's:1:"1";', +)) +->values(array( 'name' => 'upload_page', 'value' => 'b:1;', )) @@ -46033,7 +46130,7 @@ 'relations' => '1', 'hierarchy' => '0', 'multiple' => '0', - 'required' => '1', + 'required' => '0', 'tags' => '0', 'module' => 'taxonomy', 'weight' => '0', @@ -46088,10 +46185,6 @@ 'type' => 'article', )) ->values(array( - 'vid' => '4', - 'type' => 'page', -)) -->values(array( 'vid' => '1', 'type' => 'story', )) diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php index bdac5c9..5074473 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php @@ -4,7 +4,10 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\MigrateMessageInterface; use Drupal\user\Entity\User; +use Prophecy\Argument; /** * @group migrate_drupal @@ -85,4 +88,40 @@ public function testOverwriteProperties() { $this->assertIdentical('proto@zo.an', $account->getInitialEmail()); } + /** + * Test that translation destination fails for untranslatable entities. + */ + public function testUntranslatable() { + $this->enableModules(['language_test']); + $this->installEntitySchema('no_language_entity_test'); + + /** @var MigrationInterface $migration */ + $migration = \Drupal::service('plugin.manager.migration')->createStubMigration([ + 'source' => [ + 'plugin' => 'embedded_data', + 'ids' => ['id' => ['type' => 'integer']], + 'data_rows' => [['id' => 1]], + ], + 'process' => [ + 'id' => 'id', + ], + 'destination' => [ + 'plugin' => 'entity:no_language_entity_test', + 'translations' => TRUE, + ], + ]); + + $message = $this->prophesize(MigrateMessageInterface::class); + // Match the expected message. Can't use default argument types, because + // we need to convert to string from TranslatableMarkup. + $argument = Argument::that(function($msg) { + return strpos((string) $msg, "This entity type does not support translation") !== FALSE; + }); + $message->display($argument, Argument::any()) + ->shouldBeCalled(); + + $executable = new MigrateExecutable($migration, $message->reveal()); + $executable->import(); + } + } diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php index 72a8095..3a9467e 100644 --- a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php @@ -89,17 +89,24 @@ protected function migrateFields() { /** * Executes all content migrations. * - * @param bool $include_revisions - * If TRUE, migrates node revisions. + * @param array $include + * Extra things to include as part of the migrations. Values may be + * 'revisions' or 'translations'. */ - protected function migrateContent($include_revisions = FALSE) { + protected function migrateContent($include = []) { + if (in_array('translations', $include)) { + $this->executeMigrations(['language']); + } $this->migrateUsers(FALSE); $this->migrateFields(); $this->installEntitySchema('node'); $this->executeMigrations(['d6_node_settings', 'd6_node']); - if ($include_revisions) { + if (in_array('translations', $include)) { + $this->executeMigrations(['translations']); + } + if (in_array('revisions', $include)) { $this->executeMigrations(['d6_node_revision']); } } diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index 5808c1f..b437852 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -266,6 +266,14 @@ class MigrateUpgradeForm extends ConfirmFormBase { 'source_module' => 'image', 'destination_module' => 'image', ], + 'd6_language_content_settings' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], + 'd7_language_content_settings' => [ + 'source_module' => 'locale', + 'destination_module' => 'language', + ], 'd7_language_negotiation_settings' => [ 'source_module' => 'locale', 'destination_module' => 'language', @@ -290,6 +298,10 @@ class MigrateUpgradeForm extends ConfirmFormBase { 'source_module' => 'node', 'destination_module' => 'node', ], + 'd6_node_translation' => [ + 'source_module' => 'node', + 'destination_module' => 'node', + ], 'd6_node_revision' => [ 'source_module' => 'node', 'destination_module' => 'node', diff --git a/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php index 8cf8c8d..c139291 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php @@ -30,7 +30,7 @@ * * @var array */ - public static $modules = ['migrate_drupal_ui', 'telephone']; + public static $modules = ['language', 'content_translation', 'migrate_drupal_ui', 'telephone']; /** * {@inheritdoc} diff --git a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php index ea0b588..fe31771 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php +++ b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php @@ -40,14 +40,16 @@ protected function getEntityCounts() { 'comment' => 3, 'comment_type' => 2, 'contact_form' => 5, + 'configurable_language' => 5, 'editor' => 2, 'field_config' => 62, 'field_storage_config' => 43, 'file' => 7, 'filter_format' => 7, 'image_style' => 5, + 'language_content_settings' => 2, 'migration' => 105, - 'node' => 9, + 'node' => 10, 'node_type' => 11, 'rdf_mapping' => 5, 'search_page' => 2, @@ -57,7 +59,7 @@ protected function getEntityCounts() { 'menu' => 8, 'taxonomy_term' => 6, 'taxonomy_vocabulary' => 6, - 'tour' => 1, + 'tour' => 4, 'user' => 7, 'user_role' => 6, 'menu_link_content' => 4, diff --git a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php index 1d3ce61..be807e4 100644 --- a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php @@ -39,6 +39,8 @@ protected function getEntityCounts() { 'block_content_type' => 1, 'comment' => 1, 'comment_type' => 7, + // Module 'language' comes with 'en', 'und', 'zxx'. Migration adds 'is'. + 'configurable_language' => 4, 'contact_form' => 3, 'editor' => 2, 'field_config' => 41, @@ -46,6 +48,7 @@ protected function getEntityCounts() { 'file' => 1, 'filter_format' => 7, 'image_style' => 6, + 'language_content_settings' => 1, 'migration' => 59, 'node' => 2, 'node_type' => 6, @@ -57,7 +60,7 @@ protected function getEntityCounts() { 'menu' => 10, 'taxonomy_term' => 18, 'taxonomy_vocabulary' => 3, - 'tour' => 1, + 'tour' => 4, 'user' => 3, 'user_role' => 4, 'menu_link_content' => 9, diff --git a/core/modules/node/migration_templates/d6_node.yml b/core/modules/node/migration_templates/d6_node.yml index ec1474a..58aea35 100644 --- a/core/modules/node/migration_templates/d6_node.yml +++ b/core/modules/node/migration_templates/d6_node.yml @@ -6,7 +6,10 @@ deriver: Drupal\node\Plugin\migrate\D6NodeDeriver source: plugin: d6_node process: - nid: nid + # In D6, nodes always have a tnid, but it's zero for untranslated nodes. + # We normalize it to equal the nid in that case. + # @see \Drupal\node\Plugin\migrate\source\d6\Node::prepareRow(). + nid: tnid vid: vid langcode: plugin: default_value diff --git a/core/modules/node/migration_templates/d6_node_translation.yml b/core/modules/node/migration_templates/d6_node_translation.yml new file mode 100644 index 0000000..2ddd5a4 --- /dev/null +++ b/core/modules/node/migration_templates/d6_node_translation.yml @@ -0,0 +1,51 @@ +id: d6_node_translation +label: Node translations +migration_tags: + - Drupal 6 +deriver: Drupal\node\Plugin\migrate\D6NodeDeriver +source: + plugin: d6_node + translations: true +process: + nid: tnid + type: type + langcode: + plugin: default_value + source: language + default_value: "und" + title: title + uid: node_uid + status: status + created: created + changed: changed + promote: promote + sticky: sticky + 'body/format': + plugin: migration + migration: d6_filter_format + source: format + 'body/value': body + 'body/summary': teaser + revision_uid: revision_uid + revision_log: log + revision_timestamp: timestamp + +# unmapped d6 fields. +# translate +# moderate +# comment + +destination: + plugin: entity:node + translations: true +migration_dependencies: + required: + - d6_user + - d6_node_type + - d6_node_settings + - d6_filter_format + - language + optional: + - d6_field_instance_widget_settings + - d6_field_formatter_settings + - d6_upload_field_instance diff --git a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php index c8a1397..f3e1a92 100644 --- a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php +++ b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php @@ -38,25 +38,37 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface { protected $cckPluginManager; /** + * Whether or not to include translations. + * + * @var bool + */ + protected $includeTranslations; + + /** * D6NodeDeriver constructor. * * @param string $base_plugin_id * The base plugin ID for the plugin ID. * @param \Drupal\Component\Plugin\PluginManagerInterface $cck_manager * The CCK plugin manager. + * @param bool $translations + * Whether or not to include translations. */ - public function __construct($base_plugin_id, PluginManagerInterface $cck_manager) { + public function __construct($base_plugin_id, PluginManagerInterface $cck_manager, $translations) { $this->basePluginId = $base_plugin_id; $this->cckPluginManager = $cck_manager; + $this->includeTranslations = $translations; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, $base_plugin_id) { + // Translations don't make sense unless we have content_translation. return new static( $base_plugin_id, - $container->get('plugin.manager.migrate.cckfield') + $container->get('plugin.manager.migrate.cckfield'), + $container->get('module_handler')->moduleExists('content_translation') ); } @@ -72,6 +84,11 @@ public static function create(ContainerInterface $container, $base_plugin_id) { * @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinition() */ public function getDerivativeDefinitions($base_plugin_definition) { + if ($base_plugin_definition['id'] == 'd6_node_translation' && !$this->includeTranslations) { + // Refuse to generate anything. + return $this->derivatives; + } + // Read all CCK field instance definitions in the source database. $fields = array(); try { @@ -100,9 +117,10 @@ public function getDerivativeDefinitions($base_plugin_definition) { $values['source']['node_type'] = $node_type; $values['destination']['default_bundle'] = $node_type; - // If this migration is based on the d6_node_revision migration, it - // should explicitly depend on the corresponding d6_node variant. - if ($base_plugin_definition['id'] == 'd6_node_revision') { + // If this migration is based on the d6_node_revision migration or + // is for translations of nodes, it should explicitly depend on the + // corresponding d6_node variant. + if (in_array($base_plugin_definition['id'], ['d6_node_revision', 'd6_node_translation'])) { $values['migration_dependencies']['required'][] = 'd6_node:' . $node_type; } diff --git a/core/modules/node/src/Plugin/migrate/source/d6/Node.php b/core/modules/node/src/Plugin/migrate/source/d6/Node.php index 1ed96f9..5ca698f 100644 --- a/core/modules/node/src/Plugin/migrate/source/d6/Node.php +++ b/core/modules/node/src/Plugin/migrate/source/d6/Node.php @@ -2,6 +2,7 @@ namespace Drupal\node\Plugin\migrate\source\d6; +use Drupal\Core\Database\Query\SelectInterface; use Drupal\migrate\Row; use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; @@ -37,9 +38,11 @@ class Node extends DrupalSqlBase { * {@inheritdoc} */ public function query() { - // Select node in its last revision. - $query = $this->select('node_revisions', 'nr') - ->fields('n', array( + $query = $this->select('node_revisions', 'nr'); + $query->innerJoin('node', 'n', static::JOIN); + $this->handleTranslations($query); + + $query->fields('n', array( 'nid', 'type', 'language', @@ -54,17 +57,16 @@ public function query() { 'translate', )) ->fields('nr', array( - 'vid', 'title', 'body', 'teaser', 'log', 'timestamp', 'format', + 'vid', )); $query->addField('n', 'uid', 'node_uid'); $query->addField('nr', 'uid', 'revision_uid'); - $query->innerJoin('node', 'n', static::JOIN); if (isset($this->configuration['node_type'])) { $query->condition('n.type', $this->configuration['node_type']); @@ -123,6 +125,11 @@ public function prepareRow(Row $row) { } } + // Make sure we always have a translation set. + if ($row->getSourceProperty('tnid') == 0) { + $row->setSourceProperty('tnid', $row->getSourceProperty('nid')); + } + return parent::prepareRow($row); } @@ -251,4 +258,22 @@ public function getIds() { return $ids; } + /** + * Adapt our query for translations. + * + * @param \Drupal\Core\Database\Query\SelectInterface + * The generated query. + */ + protected function handleTranslations(SelectInterface $query) { + // Check whether or not we want translations. + if (empty($this->configuration['translations'])) { + // No translations: Yield untranslated nodes, or default translations. + $query->where('n.tnid = 0 OR n.tnid = n.nid'); + } + else { + // Translations: Yield only non-default translations. + $query->where('n.tnid <> 0 AND n.tnid <> n.nid'); + } + } + } diff --git a/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php b/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php index 41e7a18..3f9a580 100644 --- a/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php +++ b/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php @@ -1,6 +1,7 @@ executeMigrations(['d6_node', 'd6_node_revision']); diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeDeriverTest.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeDeriverTest.php new file mode 100644 index 0000000..abce689 --- /dev/null +++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeDeriverTest.php @@ -0,0 +1,50 @@ +pluginManager = $this->container->get('plugin.manager.migration'); + } + + /** + * Test node translation migrations with translation disabled. + */ + public function testNoTranslations() { + // Without content_translation, there should be no translation migrations. + $migrations = $this->pluginManager->createInstances('d6_node_translation'); + $this->assertSame([], $migrations, + "No node translation migrations without content_translation"); + } + + /** + * Test node translation migrations with translation enabled. + */ + public function testTranslations() { + // With content_translation, there should be translation migrations for + // each content type. + $this->enableModules(['language', 'content_translation']); + $migrations = $this->pluginManager->createInstances('d6_node_translation'); + $this->assertArrayHasKey('d6_node_translation:story', $migrations, + "Node translation migrations exist after content_translation installed"); + } + +} diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php index 5f48683..122199f 100644 --- a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php +++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php @@ -19,11 +19,16 @@ class MigrateNodeTest extends MigrateNodeTestBase { /** * {@inheritdoc} */ + public static $modules = ['language', 'content_translation']; + + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); $this->setUpMigratedFiles(); $this->installSchema('file', ['file_usage']); - $this->executeMigrations(['d6_node']); + $this->executeMigrations(['language', 'd6_node', 'd6_node_translation']); } /** @@ -85,6 +90,15 @@ public function testNode() { $this->assertSame('Buy it now', $node->field_test_link->title); $this->assertSame(['attributes' => ['target' => '_blank']], $node->field_test_link->options); + // Test that translations are working. + $node = Node::load(10); + $this->assertIdentical('en', $node->langcode->value); + $this->assertIdentical('The Real McCoy', $node->title->value); + $this->assertTrue($node->hasTranslation('fr'), "Node 10 has french translation"); + + // Node 11 is a translation of node 10, and should not be imported separately. + $this->assertNull(Node::load(11), "Node 11 doesn't exist in D8, it was a translation"); + // Rerun migration with two source database changes. // 1. Add an invalid link attributes and a different URL and // title. If only the attributes are changed the error does not occur. diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php index 0c31810..6a308ff 100644 --- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php +++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php @@ -40,7 +40,7 @@ class NodeByNodeTypeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, // Node revision fields. 'body' => 'body for node 1', @@ -64,7 +64,7 @@ class NodeByNodeTypeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 2, 'translate' => 0, // Node revision fields. 'body' => 'body for node 2', diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php index 8c78be7..d548ff3 100644 --- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php +++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php @@ -134,7 +134,7 @@ class NodeRevisionByNodeTypeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, 'vid' => 1, 'node_uid' => 1, @@ -156,7 +156,7 @@ class NodeRevisionByNodeTypeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, 'vid' => 3, 'node_uid' => 1, diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php index b1c23f8..752ed9b 100644 --- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php +++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php @@ -133,7 +133,7 @@ class NodeRevisionTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, // Node revision fields. 'vid' => 1, @@ -157,7 +157,7 @@ class NodeRevisionTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, // Node revision fields. 'vid' => 3, diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php index ce55d52..b6ce22e 100644 --- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php +++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php @@ -2,16 +2,12 @@ namespace Drupal\Tests\node\Unit\Plugin\migrate\source\d6; -use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase; - /** * Tests D6 node source plugin. * * @group node */ -class NodeTest extends MigrateSqlSourceTestCase { - - const PLUGIN_CLASS = 'Drupal\node\Plugin\migrate\source\d6\Node'; +class NodeTest extends NodeTestBase { protected $migrationConfiguration = array( 'id' => 'test', @@ -36,7 +32,7 @@ class NodeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, // Node revision fields. 'body' => 'body for node 1', @@ -60,7 +56,7 @@ class NodeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 2, 'translate' => 0, // Node revision fields. 'body' => 'body for node 2', @@ -83,7 +79,7 @@ class NodeTest extends MigrateSqlSourceTestCase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 5, 'translate' => 0, // Node revision fields. 'body' => 'body for node 5', @@ -98,79 +94,29 @@ class NodeTest extends MigrateSqlSourceTestCase { ), ), ), + array( + 'nid' => 6, + 'vid' => 6, + 'type' => 'story', + 'language' => 'en', + 'title' => 'node title 6', + 'uid' => 1, + 'status' => 1, + 'created' => 1279290909, + 'changed' => 1279308994, + 'comment' => 0, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'tnid' => 6, + 'translate' => 0, + // Node revision fields. + 'body' => 'body for node 6', + 'teaser' => 'body for node 6', + 'log' => '', + 'timestamp' => 1279308994, + 'format' => 1, + ), ); - /** - * {@inheritdoc} - */ - protected function setUp() { - $this->databaseContents['content_node_field'] = array( - array( - 'field_name' => 'field_test_four', - 'type' => 'number_float', - 'global_settings' => 'a:0:{}', - 'required' => '0', - 'multiple' => '0', - 'db_storage' => '1', - 'module' => 'number', - 'db_columns' => 'a:1:{s:5:"value";a:3:{s:4:"type";s:5:"float";s:8:"not null";b:0;s:8:"sortable";b:1;}}', - 'active' => '1', - 'locked' => '0', - ), - ); - $this->databaseContents['content_node_field_instance'] = array( - array( - 'field_name' => 'field_test_four', - 'type_name' => 'story', - 'weight' => '3', - 'label' => 'Float Field', - 'widget_type' => 'number', - 'widget_settings' => 'a:0:{}', - 'display_settings' => 'a:0:{}', - 'description' => 'An example float field.', - 'widget_module' => 'number', - 'widget_active' => '1', - ), - ); - $this->databaseContents['content_type_story'] = array( - array( - 'nid' => 5, - 'vid' => 5, - 'uid' => 5, - 'field_test_four_value' => '3.14159', - ), - ); - $this->databaseContents['system'] = array( - array( - 'type' => 'module', - 'name' => 'content', - 'schema_version' => 6001, - 'status' => TRUE, - ), - ); - foreach ($this->expectedResults as $k => $row) { - foreach (array('nid', 'vid', 'title', 'uid', 'body', 'teaser', 'format', 'timestamp', 'log') as $field) { - $this->databaseContents['node_revisions'][$k][$field] = $row[$field]; - switch ($field) { - case 'nid': case 'vid': - break; - case 'uid': - $this->databaseContents['node_revisions'][$k]['uid']++; - break; - default: - unset($row[$field]); - break; - } - } - $this->databaseContents['node'][$k] = $row; - } - array_walk($this->expectedResults, function (&$row) { - $row['node_uid'] = $row['uid']; - $row['revision_uid'] = $row['uid'] + 1; - unset($row['uid']); - }); - - parent::setUp(); - } - } diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTestBase.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTestBase.php new file mode 100644 index 0000000..8850fc4 --- /dev/null +++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTestBase.php @@ -0,0 +1,179 @@ +databaseContents['content_node_field'] = array( + array( + 'field_name' => 'field_test_four', + 'type' => 'number_float', + 'global_settings' => 'a:0:{}', + 'required' => '0', + 'multiple' => '0', + 'db_storage' => '1', + 'module' => 'number', + 'db_columns' => 'a:1:{s:5:"value";a:3:{s:4:"type";s:5:"float";s:8:"not null";b:0;s:8:"sortable";b:1;}}', + 'active' => '1', + 'locked' => '0', + ), + ); + $this->databaseContents['content_node_field_instance'] = array( + array( + 'field_name' => 'field_test_four', + 'type_name' => 'story', + 'weight' => '3', + 'label' => 'Float Field', + 'widget_type' => 'number', + 'widget_settings' => 'a:0:{}', + 'display_settings' => 'a:0:{}', + 'description' => 'An example float field.', + 'widget_module' => 'number', + 'widget_active' => '1', + ), + ); + $this->databaseContents['content_type_story'] = array( + array( + 'nid' => 5, + 'vid' => 5, + 'uid' => 5, + 'field_test_four_value' => '3.14159', + ), + ); + $this->databaseContents['system'] = array( + array( + 'type' => 'module', + 'name' => 'content', + 'schema_version' => 6001, + 'status' => TRUE, + ), + ); + $this->databaseContents['node'] = [ + [ + 'nid' => 1, + 'vid' => 1, + 'type' => 'page', + 'language' => 'en', + 'title' => 'node title 1', + 'uid' => 1, + 'status' => 1, + 'created' => 1279051598, + 'changed' => 1279051598, + 'comment' => 2, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'translate' => 0, + 'tnid' => 0, + ], + [ + 'nid' => 2, + 'vid' => 2, + 'type' => 'page', + 'language' => 'en', + 'title' => 'node title 2', + 'uid' => 1, + 'status' => 1, + 'created' => 1279290908, + 'changed' => 1279308993, + 'comment' => 0, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'translate' => 0, + 'tnid' => 0, + ], + [ + 'nid' => 5, + 'vid' => 5, + 'type' => 'story', + 'language' => 'en', + 'title' => 'node title 5', + 'uid' => 1, + 'status' => 1, + 'created' => 1279290908, + 'changed' => 1279308993, + 'comment' => 0, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'translate' => 0, + 'tnid' => 0, + ], + [ + 'nid' => 6, + 'vid' => 6, + 'type' => 'story', + 'language' => 'en', + 'title' => 'node title 6', + 'uid' => 1, + 'status' => 1, + 'created' => 1279290909, + 'changed' => 1279308994, + 'comment' => 0, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'translate' => 0, + 'tnid' => 6, + ], + [ + 'nid' => 7, + 'vid' => 7, + 'type' => 'story', + 'language' => 'fr', + 'title' => 'node title 7', + 'uid' => 1, + 'status' => 1, + 'created' => 1279290910, + 'changed' => 1279308995, + 'comment' => 0, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'translate' => 0, + 'tnid' => 6, + ], + ]; + + foreach ($this->databaseContents['node'] as $k => $row) { + // Find the equivalent row from expected results. + $result_row = NULL; + foreach ($this->expectedResults as $result) { + if (in_array($result['nid'], [$row['nid'], $row['tnid']]) && $result['language'] == $row['language']) { + $result_row = $result; + break; + } + } + + // Populate node_revisions. + foreach (array('nid', 'vid', 'title', 'uid', 'body', 'teaser', 'format', 'timestamp', 'log') as $field) { + $value = isset($row[$field]) ? $row[$field] : $result_row[$field]; + $this->databaseContents['node_revisions'][$k][$field] = $value; + if ($field == 'uid') { + $this->databaseContents['node_revisions'][$k]['uid']++; + } + } + } + + array_walk($this->expectedResults, function (&$row) { + $row['node_uid'] = $row['uid']; + $row['revision_uid'] = $row['uid'] + 1; + unset($row['uid']); + }); + + parent::setUp(); + } + +} diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTranslationTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTranslationTest.php new file mode 100644 index 0000000..da0b167 --- /dev/null +++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTranslationTest.php @@ -0,0 +1,46 @@ + 'test', + 'source' => array( + 'plugin' => 'd6_node', + 'translations' => TRUE, + ), + ); + + protected $expectedResults = array( + array( + 'nid' => 7, + 'vid' => 7, + 'type' => 'story', + 'language' => 'fr', + 'title' => 'node title 7', + 'uid' => 1, + 'status' => 1, + 'created' => 1279290910, + 'changed' => 1279308995, + 'comment' => 0, + 'promote' => 1, + 'moderate' => 0, + 'sticky' => 0, + 'tnid' => 6, + 'translate' => 0, + // Node revision fields. + 'body' => 'body for node 7', + 'teaser' => 'body for node 7', + 'log' => '', + 'timestamp' => 1279308995, + 'format' => 1, + ), + ); + +} diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php index ff2b514..facd69b 100644 --- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php @@ -22,7 +22,7 @@ class MigrateTermNodeRevisionTest extends MigrateDrupal6TestBase { protected function setUp() { parent::setUp(); $this->installSchema('node', ['node_access']); - $this->migrateContent(TRUE); + $this->migrateContent(['revisions']); $this->migrateTaxonomy(); $this->executeMigrations(['d6_term_node', 'd6_term_node_revision']); } diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php index 718c35d..26f069c 100644 --- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php @@ -55,7 +55,7 @@ public function testTermNode() { public function testSkipNonExistentNode() { // Node 2 is migrated by d6_node__story, but we need to pretend that it // failed, so record that in the map table. - $this->mockFailure('d6_node:story', ['nid' => 2]); + $this->mockFailure('d6_node:story', ['nid' => 2, 'language' => 'en']); // d6_term_node__2 should skip over node 2 (a.k.a. revision 3) because, // according to the map table, it failed.