Commit 1fa26afd authored by alexpott's avatar alexpott

Issue #2225775 by vasi, quietone, phenaproxima, chx, penyaskito, mikeryan,...

Issue #2225775 by vasi, quietone, phenaproxima, chx, penyaskito, mikeryan, esclapes, vprocessor, steinmb, Gábor Hojtsy, Marc Angles, bwinett: Migrate Drupal 6 core node translation to Drupal 8
parent 89815ba3
......@@ -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;
/**
......@@ -74,11 +77,27 @@ public function onConfigImporterImport() {
$this->updateDefinitions($entity_types);
}
/**
* 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;
}
......
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
......@@ -39,6 +39,8 @@ process:
2: true
destination:
plugin: entity:language_content_settings
content_translation_update_definitions:
- node
migration_dependencies:
required:
- d7_node_type
......@@ -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;
}
}
......
......@@ -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.
......
......@@ -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;
}
/**
......@@ -104,12 +110,32 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i
return array($entity->id());
}
/**
* 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);
}
}
}
name: 'Migration external translated test'
type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- node
- migrate
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
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
<?php
namespace Drupal\migrate_external_translated_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* A simple migrate source for our tests.
*
* @MigrateSource(
* id = "migrate_external_translated_test"
* )
*/
class MigrateExternalTranslatedTestSource extends SourcePluginBase {
/**
* The data to import.
*
* @var array
*/
protected $import = [
['name' => '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);
}
}
<?php
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Tests the EntityContentBase destination.
*
* @group migrate
*/
class MigrateEntityContentBaseTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['migrate', 'user', 'language', 'entity_test'];
/**
* The storage for entity_test_mul.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* A content migrate destination.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationInterface
*/
protected $destination;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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');
}
}
<?php
namespace Drupal\Tests\migrate\Kernel;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateExecutable;
use Drupal\node\Entity\NodeType;
/**
* Tests migrating non-Drupal translated content.
*
* Ensure it's possible to migrate in translations, even if there's no nid or
* tnid property on the source.
*
* @group migrate
*/
class MigrateExternalTranslatedTest extends MigrateTestBase {
/**
* {@inheritdoc}
*
* @todo: Remove migrate_drupal when https://www.drupal.org/node/2560795 is
* fixed.
*/
public static $modules = ['system', 'user', 'language', 'node', 'field', 'migrate_drupal', 'migrate_external_translated_test'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->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()));
}
}
......@@ -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();
}
}
/**
......
......@@ -8034,7 +8034,17 @@
->values(array(
'uid' => '1',
'nid' => '9',
'timestamp' => '1457655127',
'timestamp' => '1468384961',
))
->values(array(
'uid' => '1',
'nid' => '12',
'timestamp' => '1468384823',
))
->values(array(