diff --git a/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php b/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php index cd88ba1..9c2af9a 100644 --- a/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php +++ b/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php @@ -25,6 +25,7 @@ class MigrateCommentTest extends MigrateDrupal6TestBase { protected function setUp() { parent::setUp(); + $this->installSchema('node', ['node_access']); $this->installEntitySchema('node'); $this->installEntitySchema('comment'); $this->installSchema('comment', ['comment_entity_statistics']); 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/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index 9ed7841..ff22336 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -150,6 +150,9 @@ public function getIds() { * The row object to update from. */ 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 ($entity instanceof TranslatableInterface) { $property = $this->storage->getEntityType()->getKey('langcode'); @@ -157,6 +160,9 @@ protected function updateEntity(EntityInterface $entity, Row $row) { $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); } @@ -180,7 +186,7 @@ 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; @@ -228,4 +234,32 @@ protected function processStubRow(Row $row) { } } + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifier) { + if (empty($this->configuration['translations'])) { + parent::rollback($destination_identifier); + } + else { + // 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(); + } + } + } + } + } + } + } + } 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/migration_templates/migrate.migration.external_translated_test_node.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migration_templates/migrate.migration.external_translated_test_node.yml new file mode 100644 index 0000000..8095841 --- /dev/null +++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migration_templates/migrate.migration.external_translated_test_node.yml @@ -0,0 +1,20 @@ +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: + 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/migration_templates/migrate.migration.external_translated_test_node_translation.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migration_templates/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/migration_templates/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..eb3692e --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php @@ -0,0 +1,94 @@ +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); + $this->assertFalse($node->hasTranslation('de')); + + $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')); + $this->assertFalse($node->hasTranslation('de')); + + $node = $storage->load(3); + $this->assertEquals('en', $node->language()->getId()); + $this->assertEquals('Monkey', $node->title->value); + $this->assertFalse($node->hasTranslation('fr')); + $this->assertFalse($node->hasTranslation('es')); + $this->assertFalse($node->hasTranslation('de')); + + $this->assertNull($storage->load(4)); + + // 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_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php index 643c116..23447ce 100644 --- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php +++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php @@ -290,6 +290,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/node/migration_templates/d6_node.yml b/core/modules/node/migration_templates/d6_node.yml index 82571b8..705b556 100644 --- a/core/modules/node/migration_templates/d6_node.yml +++ b/core/modules/node/migration_templates/d6_node.yml @@ -6,7 +6,7 @@ deriver: Drupal\node\Plugin\migrate\D6NodeDeriver source: plugin: d6_node process: - nid: nid + nid: tnid vid: vid type: type langcode: diff --git a/core/modules/node/migration_templates/d6_node_translation.yml b/core/modules/node/migration_templates/d6_node_translation.yml index 9a33acd..b9d279f 100644 --- a/core/modules/node/migration_templates/d6_node_translation.yml +++ b/core/modules/node/migration_templates/d6_node_translation.yml @@ -1,19 +1,13 @@ id: d6_node_translation -label: Nodes translations +label: Node translations migration_tags: - Drupal 6 deriver: Drupal\node\Plugin\migrate\D6NodeDeriver source: plugin: d6_node + translations: true process: - nid: - - - plugin: migration - migration: d6_node - source: nid - - - plugin: skip_on_empty - method: row + nid: tnid type: type langcode: plugin: default_value 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 5379843..50018a1 100644 --- a/core/modules/node/src/Plugin/migrate/source/d6/Node.php +++ b/core/modules/node/src/Plugin/migrate/source/d6/Node.php @@ -65,14 +65,11 @@ public function query() { 'log', 'timestamp', 'format', + 'vid', )); $query->addField('n', 'uid', 'node_uid'); $query->addField('nr', 'uid', 'revision_uid'); - // Whatever we claim this revision is, use the actual field from - // node_revisions to get field values. - $query->addField('nr', 'vid', 'vid_for_fields'); - if (isset($this->configuration['node_type'])) { $query->condition('n.type', $this->configuration['node_type']); } @@ -130,9 +127,9 @@ public function prepareRow(Row $row) { } } - // Use the same nid for translation sets. - if ($tnid = $row->getSourceProperty('tnid')) { - $row->setSourceProperty('nid', $tnid); + // Make sure we always have a translation set. + if ($row->getSourceProperty('tnid') == 0) { + $row->setSourceProperty('tnid', $row->getSourceProperty('nid')); } return parent::prepareRow($row); @@ -245,7 +242,7 @@ protected function getCckData(array $field, Row $node) { // the time being. ->isNotNull($field['field_name'] . '_' . $columns[0]) ->condition('nid', $node->getSourceProperty('nid')) - ->condition('vid', $node->getSourceProperty('vid_for_fields')) + ->condition('vid', $node->getSourceProperty('vid')) ->execute() ->fetchAllAssoc('delta'); } @@ -264,7 +261,7 @@ public function getIds() { } /** - * Build a query to get the maximum vid of each translation set. + * Wuery to get the max vid of the translation set in the node table. * * @return \Drupal\Core\Database\Query\SelectInterface * The generated query. @@ -290,8 +287,7 @@ protected function translationQuery() { $query->innerJoin('node', 'n', static::JOIN); // Claim our vid is the maximum vid of our translation set. - // Otherwise we generate translations of the same node with different - // revisions, which confuses Drupal. + // Otherwise the revision the node is assigned in D8 may be confusing. $query->join($this->maxVidQuery(), 'max_vid', 'max_vid.translation_set IN (n.nid, n.tnid)'); $query->fields('max_vid', ['vid']); 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 e6894d4..ee0f9b8 100644 --- a/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php +++ b/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php @@ -43,7 +43,6 @@ public function getIds() { protected function translationQuery() { $query = $this->select('node_revisions', 'nr'); $query->innerJoin('node', 'n', static::JOIN); - $query->addField('nr', 'vid'); return $query; } 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 456e2be..18b7413 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 @@ -32,7 +32,7 @@ class NodeTest extends NodeTestBase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 1, 'translate' => 0, // Node revision fields. 'body' => 'body for node 1', @@ -56,7 +56,7 @@ class NodeTest extends NodeTestBase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 2, 'translate' => 0, // Node revision fields. 'body' => 'body for node 2', @@ -79,7 +79,7 @@ class NodeTest extends NodeTestBase { 'promote' => 1, 'moderate' => 0, 'sticky' => 0, - 'tnid' => 0, + 'tnid' => 5, 'translate' => 0, // Node revision fields. 'body' => 'body for node 5', 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 index 6dfca19..da0b167 100644 --- 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 @@ -19,7 +19,7 @@ class NodeTranslationTest extends NodeTestBase { protected $expectedResults = array( array( - 'nid' => 6, + 'nid' => 7, 'vid' => 7, 'type' => 'story', 'language' => 'fr',