Issue #2940204 by plach, Wim Leers, effulgentsia, matsbla: Translatable fields with synchronization enabled should behave as untranslatable fields with respect to pending revisions
parent
9c783555d0
commit
bbfa34e779
|
@ -163,6 +163,8 @@ function content_translation_entity_type_alter(array &$entity_types) {
|
||||||
}
|
}
|
||||||
$entity_type->set('translation', $translation);
|
$entity_type->set('translation', $translation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$entity_type->addConstraint('ContentTranslationSynchronizedFields');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
services:
|
services:
|
||||||
content_translation.synchronizer:
|
content_translation.synchronizer:
|
||||||
class: Drupal\content_translation\FieldTranslationSynchronizer
|
class: Drupal\content_translation\FieldTranslationSynchronizer
|
||||||
arguments: ['@entity.manager']
|
arguments: ['@entity.manager', '@plugin.manager.field.field_type']
|
||||||
|
|
||||||
content_translation.subscriber:
|
content_translation.subscriber:
|
||||||
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
|
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
|
||||||
|
|
|
@ -5,6 +5,8 @@ namespace Drupal\content_translation;
|
||||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||||
use Drupal\Core\Entity\ContentEntityInterface;
|
use Drupal\Core\Entity\ContentEntityInterface;
|
||||||
use Drupal\Core\Entity\EntityManagerInterface;
|
use Drupal\Core\Entity\EntityManagerInterface;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides field translation synchronization capabilities.
|
* Provides field translation synchronization capabilities.
|
||||||
|
@ -18,14 +20,57 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||||
*/
|
*/
|
||||||
protected $entityManager;
|
protected $entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The field type plugin manager.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
|
||||||
|
*/
|
||||||
|
protected $fieldTypeManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a FieldTranslationSynchronizer object.
|
* Constructs a FieldTranslationSynchronizer object.
|
||||||
*
|
*
|
||||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
|
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
|
||||||
* The entity manager.
|
* The entity manager.
|
||||||
|
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
|
||||||
|
* The field type plugin manager.
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityManagerInterface $entityManager) {
|
public function __construct(EntityManagerInterface $entityManager, FieldTypePluginManagerInterface $field_type_manager) {
|
||||||
$this->entityManager = $entityManager;
|
$this->entityManager = $entityManager;
|
||||||
|
$this->fieldTypeManager = $field_type_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition) {
|
||||||
|
$properties = [];
|
||||||
|
$settings = $this->getFieldSynchronizationSettings($field_definition);
|
||||||
|
foreach ($settings as $group => $translatable) {
|
||||||
|
if (!$translatable) {
|
||||||
|
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
|
||||||
|
if (!empty($field_type_definition['column_groups'][$group]['columns'])) {
|
||||||
|
$properties = array_merge($properties, $field_type_definition['column_groups'][$group]['columns']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the synchronization settings for the specified field.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||||
|
* A field definition.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* An array of synchronized field property names.
|
||||||
|
*/
|
||||||
|
protected function getFieldSynchronizationSettings(FieldDefinitionInterface $field_definition) {
|
||||||
|
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable()) {
|
||||||
|
return $field_definition->getThirdPartySetting('content_translation', 'translation_sync', []);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +78,6 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||||
*/
|
*/
|
||||||
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
|
public function synchronizeFields(ContentEntityInterface $entity, $sync_langcode, $original_langcode = NULL) {
|
||||||
$translations = $entity->getTranslationLanguages();
|
$translations = $entity->getTranslationLanguages();
|
||||||
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
|
|
||||||
|
|
||||||
// If we have no information about what to sync to, if we are creating a new
|
// If we have no information about what to sync to, if we are creating a new
|
||||||
// entity, if we have no translations for the current entity and we are not
|
// entity, if we have no translations for the current entity and we are not
|
||||||
|
@ -43,21 +87,55 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the entity language is being changed there is nothing to synchronize.
|
// If the entity language is being changed there is nothing to synchronize.
|
||||||
$entity_type = $entity->getEntityTypeId();
|
$entity_unchanged = $this->getOriginalEntity($entity);
|
||||||
$entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id());
|
|
||||||
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
|
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($entity->isNewRevision()) {
|
||||||
|
if ($entity->isDefaultTranslationAffectedOnly()) {
|
||||||
|
// If changes to untranslatable fields are configured to affect only the
|
||||||
|
// default translation, we need to skip synchronization in pending
|
||||||
|
// revisions, otherwise multiple translations would be affected.
|
||||||
|
if (!$entity->isDefaultRevision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// When this mode is enabled, changes to synchronized properties are
|
||||||
|
// allowed only in the default translation, thus we need to make sure this
|
||||||
|
// is always used as source for the synchronization process.
|
||||||
|
else {
|
||||||
|
$sync_langcode = $entity->getUntranslated()->language()->getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($entity->isDefaultRevision()) {
|
||||||
|
// If a new default revision is being saved, but a newer default
|
||||||
|
// revision was created meanwhile, use any other translation as source
|
||||||
|
// for synchronization, since that will have been merged from the
|
||||||
|
// default revision. In this case the actual language does not matter as
|
||||||
|
// synchronized properties are the same for all the translations in the
|
||||||
|
// default revision.
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $default_revision */
|
||||||
|
$default_revision = $this->entityManager
|
||||||
|
->getStorage($entity->getEntityTypeId())
|
||||||
|
->load($entity->id());
|
||||||
|
if ($default_revision->getLoadedRevisionId() !== $entity->getLoadedRevisionId()) {
|
||||||
|
$other_langcodes = array_diff_key($default_revision->getTranslationLanguages(), [$sync_langcode => FALSE]);
|
||||||
|
if ($other_langcodes) {
|
||||||
|
$sync_langcode = key($other_langcodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
|
/** @var \Drupal\Core\Field\FieldItemListInterface $items */
|
||||||
foreach ($entity as $field_name => $items) {
|
foreach ($entity as $field_name => $items) {
|
||||||
$field_definition = $items->getFieldDefinition();
|
$field_definition = $items->getFieldDefinition();
|
||||||
$field_type_definition = $field_type_manager->getDefinition($field_definition->getType());
|
$field_type_definition = $this->fieldTypeManager->getDefinition($field_definition->getType());
|
||||||
$column_groups = $field_type_definition['column_groups'];
|
$column_groups = $field_type_definition['column_groups'];
|
||||||
|
|
||||||
// Sync if the field is translatable, not empty, and the synchronization
|
// Sync if the field is translatable, not empty, and the synchronization
|
||||||
// setting is enabled.
|
// setting is enabled.
|
||||||
if ($field_definition instanceof ThirdPartySettingsInterface && $field_definition->isTranslatable() && !$items->isEmpty() && $translation_sync = $field_definition->getThirdPartySetting('content_translation', 'translation_sync')) {
|
if (($translation_sync = $this->getFieldSynchronizationSettings($field_definition)) && !$items->isEmpty()) {
|
||||||
// Retrieve all the untranslatable column groups and merge them into
|
// Retrieve all the untranslatable column groups and merge them into
|
||||||
// single list.
|
// single list.
|
||||||
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
|
$groups = array_keys(array_diff($translation_sync, array_filter($translation_sync)));
|
||||||
|
@ -101,6 +179,26 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original unchanged entity to be used to detect changes.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||||
|
* The entity being changed.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||||
|
* The unchanged entity.
|
||||||
|
*/
|
||||||
|
protected function getOriginalEntity(ContentEntityInterface $entity) {
|
||||||
|
if (!isset($entity->original)) {
|
||||||
|
$storage = $this->entityManager->getStorage($entity->getEntityTypeId());
|
||||||
|
$original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$original = $entity->original;
|
||||||
|
}
|
||||||
|
return $original;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -174,9 +272,7 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||||
// items and the other columns from the existing values. This only
|
// items and the other columns from the existing values. This only
|
||||||
// works if the delta exists in the language.
|
// works if the delta exists in the language.
|
||||||
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
|
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
|
||||||
$item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($columns));
|
$values[$langcode][$delta] = $this->createMergedItem($source_items[$delta], $original_field_values[$langcode][$delta], $columns);
|
||||||
$item_columns_to_keep = array_diff_key($original_field_values[$langcode][$delta], array_flip($columns));
|
|
||||||
$values[$langcode][$delta] = $item_columns_to_sync + $item_columns_to_keep;
|
|
||||||
}
|
}
|
||||||
// If the delta doesn't exist, copy from the source language.
|
// If the delta doesn't exist, copy from the source language.
|
||||||
elseif ($created) {
|
elseif ($created) {
|
||||||
|
@ -190,13 +286,37 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||||
// If the value has only been reordered we just move the old one in
|
// If the value has only been reordered we just move the old one in
|
||||||
// the new position.
|
// the new position.
|
||||||
$item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta];
|
$item = isset($original_field_values[$langcode][$old_delta]) ? $original_field_values[$langcode][$old_delta] : $source_items[$new_delta];
|
||||||
$values[$langcode][$new_delta] = $item;
|
// When saving a default revision starting from a pending revision,
|
||||||
|
// we may have desynchronized field values, so we make sure that
|
||||||
|
// untranslatable properties are synchronized, even if in any other
|
||||||
|
// situation this would not be necessary.
|
||||||
|
$values[$langcode][$new_delta] = $this->createMergedItem($source_items[$new_delta], $item, $columns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a merged item.
|
||||||
|
*
|
||||||
|
* @param array $source_item
|
||||||
|
* An item containing the untranslatable properties to be synchronized.
|
||||||
|
* @param array $target_item
|
||||||
|
* An item containing the translatable properties to be kept.
|
||||||
|
* @param string[] $properties
|
||||||
|
* An array of properties to be synchronized.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* A merged item array.
|
||||||
|
*/
|
||||||
|
protected function createMergedItem(array $source_item, array $target_item, array $properties) {
|
||||||
|
$property_keys = array_flip($properties);
|
||||||
|
$item_properties_to_sync = array_intersect_key($source_item, $property_keys);
|
||||||
|
$item_properties_to_keep = array_diff_key($target_item, $property_keys);
|
||||||
|
return $item_properties_to_sync + $item_properties_to_keep;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes a hash code for the specified item.
|
* Computes a hash code for the specified item.
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Drupal\content_translation;
|
namespace Drupal\content_translation;
|
||||||
|
|
||||||
use Drupal\Core\Entity\ContentEntityInterface;
|
use Drupal\Core\Entity\ContentEntityInterface;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides field translation synchronization capabilities.
|
* Provides field translation synchronization capabilities.
|
||||||
|
@ -54,4 +55,15 @@ interface FieldTranslationSynchronizerInterface {
|
||||||
*/
|
*/
|
||||||
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns);
|
public function synchronizeItems(array &$field_values, array $unchanged_items, $sync_langcode, array $translations, array $columns);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the synchronized properties for the specified field definition.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||||
|
* A field definition.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* An array of synchronized field property names.
|
||||||
|
*/
|
||||||
|
public function getFieldSynchronizedProperties(FieldDefinitionInterface $field_definition);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\content_translation\Plugin\Validation\Constraint;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation constraint for the entity changed timestamp.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @Constraint(
|
||||||
|
* id = "ContentTranslationSynchronizedFields",
|
||||||
|
* label = @Translation("Content translation synchronized fields", context = "Validation"),
|
||||||
|
* type = {"entity"}
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class ContentTranslationSynchronizedFieldsConstraint extends Constraint {
|
||||||
|
|
||||||
|
// In this case "elements" refers to "field properties", in fact it is what we
|
||||||
|
// are using in the UI elsewhere.
|
||||||
|
public $defaultRevisionMessage = 'Non-translatable field elements can only be changed when updating the current revision.';
|
||||||
|
public $defaultTranslationMessage = 'Non-translatable field elements can only be changed when updating the original language.';
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\content_translation\Plugin\Validation\Constraint;
|
||||||
|
|
||||||
|
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||||
|
use Drupal\content_translation\FieldTranslationSynchronizerInterface;
|
||||||
|
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||||
|
use Drupal\Core\Entity\ContentEntityInterface;
|
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||||
|
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that synchronized fields are handled correctly in pending revisions.
|
||||||
|
*
|
||||||
|
* As for untranslatable fields, two modes are supported:
|
||||||
|
* - When changes to untranslatable fields are configured to affect all revision
|
||||||
|
* translations, synchronized field properties can be changed only in default
|
||||||
|
* revisions.
|
||||||
|
* - When changes to untranslatable fields affect are configured to affect only
|
||||||
|
* the revision's default translation, synchronized field properties can be
|
||||||
|
* changed only when editing the default translation. This may lead to
|
||||||
|
* temporarily desynchronized values, when saving a pending revision for the
|
||||||
|
* default translation that changes a synchronized property. These are
|
||||||
|
* actually synchronized when saving changes to the default translation as a
|
||||||
|
* new default revision.
|
||||||
|
*
|
||||||
|
* @see \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint
|
||||||
|
* @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entity type manager.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||||
|
*/
|
||||||
|
protected $entityTypeManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content translation manager.
|
||||||
|
*
|
||||||
|
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||||
|
*/
|
||||||
|
protected $contentTranslationManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The field translation synchronizer.
|
||||||
|
*
|
||||||
|
* @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
|
||||||
|
*/
|
||||||
|
protected $synchronizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ContentTranslationSynchronizedFieldsConstraintValidator constructor.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||||
|
* The entity type manager.
|
||||||
|
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||||
|
* The content translation manager.
|
||||||
|
* @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
|
||||||
|
* The field translation synchronizer.
|
||||||
|
*/
|
||||||
|
public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
|
||||||
|
$this->entityTypeManager = $entity_type_manager;
|
||||||
|
$this->contentTranslationManager = $content_translation_manager;
|
||||||
|
$this->synchronizer = $synchronizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function create(ContainerInterface $container) {
|
||||||
|
return new static(
|
||||||
|
$container->get('entity_type.manager'),
|
||||||
|
$container->get('content_translation.manager'),
|
||||||
|
$container->get('content_translation.synchronizer')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validate($value, Constraint $constraint) {
|
||||||
|
/** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||||
|
$entity = $value;
|
||||||
|
if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// When changes to untranslatable fields are configured to affect all
|
||||||
|
// revision translations, we always allow changes in default revisions.
|
||||||
|
if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$entity_type_id = $entity->getEntityTypeId();
|
||||||
|
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions());
|
||||||
|
if (!$synchronized_properties) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $original */
|
||||||
|
$original = $this->getOriginalEntity($entity);
|
||||||
|
$original_translation = $this->getOriginalTranslation($entity, $original);
|
||||||
|
if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
|
||||||
|
if ($entity->isDefaultTranslationAffectedOnly()) {
|
||||||
|
foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
|
||||||
|
if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
|
||||||
|
$this->context->addViolation($constraint->defaultTranslationMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->context->addViolation($constraint->defaultRevisionMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether any synchronized property has changes.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||||
|
* The entity being validated.
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $original
|
||||||
|
* The original unchanged entity.
|
||||||
|
* @param string[][] $synchronized_properties
|
||||||
|
* An associative array of arrays of synchronized field properties keyed by
|
||||||
|
* field name.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* TRUE if changes in synchronized properties were detected, FALSE
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
|
||||||
|
foreach ($synchronized_properties as $field_name => $properties) {
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
$items = $entity->get($field_name)->getValue();
|
||||||
|
$original_items = $original->get($field_name)->getValue();
|
||||||
|
if (count($items) !== count($original_items)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
foreach ($items as $delta => $item) {
|
||||||
|
// @todo This loose comparison is not fully reliable. Revisit this
|
||||||
|
// after https://www.drupal.org/project/drupal/issues/2941092.
|
||||||
|
if ($items[$delta][$property] != $original_items[$delta][$property]) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original unchanged entity to be used to detect changes.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||||
|
* The entity being changed.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||||
|
* The unchanged entity.
|
||||||
|
*/
|
||||||
|
protected function getOriginalEntity(ContentEntityInterface $entity) {
|
||||||
|
if (!isset($entity->original)) {
|
||||||
|
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
|
||||||
|
$original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$original = $entity->original;
|
||||||
|
}
|
||||||
|
return $original;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original translation.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||||
|
* The entity being validated.
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $original
|
||||||
|
* The original entity.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||||
|
* The original entity translation object.
|
||||||
|
*/
|
||||||
|
protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {
|
||||||
|
$langcode = $entity->language()->getId();
|
||||||
|
if ($original->hasTranslation($langcode)) {
|
||||||
|
$original_langcode = $langcode;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
|
||||||
|
$original_langcode = $metadata->getSource();
|
||||||
|
}
|
||||||
|
return $original->getTranslation($original_langcode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the synchronized properties for every specified field.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
|
||||||
|
* An array of field definitions.
|
||||||
|
*
|
||||||
|
* @return string[][]
|
||||||
|
* An associative array of arrays of field property names keyed by field
|
||||||
|
* name.
|
||||||
|
*/
|
||||||
|
public function getSynchronizedPropertiesByField(array $field_definitions) {
|
||||||
|
$synchronizer = $this->synchronizer;
|
||||||
|
$synchronized_properties = array_filter(array_map(
|
||||||
|
function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
|
||||||
|
return $synchronizer->getFieldSynchronizedProperties($field_definition);
|
||||||
|
},
|
||||||
|
$field_definitions
|
||||||
|
));
|
||||||
|
return $synchronized_properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,10 @@
|
||||||
* Helper module for the Content Translation tests.
|
* Helper module for the Content Translation tests.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use Drupal\Core\Access\AccessResult;
|
||||||
|
use Drupal\Core\Entity\EntityInterface;
|
||||||
use Drupal\Core\Form\FormStateInterface;
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
use Drupal\Core\Session\AccountInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements hook_entity_bundle_info_alter().
|
* Implements hook_entity_bundle_info_alter().
|
||||||
|
@ -23,6 +26,19 @@ function content_translation_test_entity_bundle_info_alter(&$bundles) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements hook_entity_access().
|
||||||
|
*/
|
||||||
|
function content_translation_test_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||||
|
$access = \Drupal::state()->get('content_translation.entity_access.' . $entity->getEntityTypeId());
|
||||||
|
if (!empty($access[$operation])) {
|
||||||
|
return AccessResult::allowed();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return AccessResult::neutral();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements hook_form_BASE_FORM_ID_alter().
|
* Implements hook_form_BASE_FORM_ID_alter().
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,482 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\Tests\content_translation\Kernel;
|
||||||
|
|
||||||
|
use Drupal\Core\Entity\ContentEntityInterface;
|
||||||
|
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
|
||||||
|
use Drupal\Core\Language\LanguageInterface;
|
||||||
|
use Drupal\entity_test\Entity\EntityTestMulRev;
|
||||||
|
use Drupal\field\Entity\FieldConfig;
|
||||||
|
use Drupal\field\Entity\FieldStorageConfig;
|
||||||
|
use Drupal\file\Entity\File;
|
||||||
|
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||||
|
use Drupal\language\Entity\ConfigurableLanguage;
|
||||||
|
use Drupal\Tests\TestFileCreationTrait;
|
||||||
|
use Drupal\user\Entity\User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the field synchronization logic when revisions are involved.
|
||||||
|
*
|
||||||
|
* @group content_translation
|
||||||
|
*/
|
||||||
|
class ContentTranslationFieldSyncRevisionTest extends EntityKernelTestBase {
|
||||||
|
|
||||||
|
use TestFileCreationTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static $modules = ['file', 'image', 'language', 'content_translation', 'content_translation_test'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The synchronized field name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $fieldName = 'sync_field';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content translation manager.
|
||||||
|
*
|
||||||
|
* @var \Drupal\content_translation\ContentTranslationManagerInterface|\Drupal\content_translation\BundleTranslationSettingsInterface
|
||||||
|
*/
|
||||||
|
protected $contentTranslationManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The test entity storage.
|
||||||
|
*
|
||||||
|
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
|
||||||
|
*/
|
||||||
|
protected $storage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function setUp() {
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$entity_type_id = 'entity_test_mulrev';
|
||||||
|
$this->installEntitySchema($entity_type_id);
|
||||||
|
$this->installEntitySchema('file');
|
||||||
|
$this->installSchema('file', ['file_usage']);
|
||||||
|
|
||||||
|
ConfigurableLanguage::createFromLangcode('it')->save();
|
||||||
|
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||||
|
|
||||||
|
/** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */
|
||||||
|
$field_storage_config = FieldStorageConfig::create([
|
||||||
|
'field_name' => $this->fieldName,
|
||||||
|
'type' => 'image',
|
||||||
|
'entity_type' => $entity_type_id,
|
||||||
|
'cardinality' => 1,
|
||||||
|
'translatable' => 1,
|
||||||
|
]);
|
||||||
|
$field_storage_config->save();
|
||||||
|
|
||||||
|
$field_config = FieldConfig::create([
|
||||||
|
'entity_type' => $entity_type_id,
|
||||||
|
'field_name' => $this->fieldName,
|
||||||
|
'bundle' => $entity_type_id,
|
||||||
|
'label' => 'Synchronized field',
|
||||||
|
'translatable' => 1,
|
||||||
|
]);
|
||||||
|
$field_config->save();
|
||||||
|
|
||||||
|
$property_settings = [
|
||||||
|
'alt' => 'alt',
|
||||||
|
'title' => 'title',
|
||||||
|
'file' => 0,
|
||||||
|
];
|
||||||
|
$field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings);
|
||||||
|
$field_config->save();
|
||||||
|
|
||||||
|
$this->entityManager->clearCachedDefinitions();
|
||||||
|
|
||||||
|
$this->contentTranslationManager = $this->container->get('content_translation.manager');
|
||||||
|
$this->contentTranslationManager->setEnabled($entity_type_id, $entity_type_id, TRUE);
|
||||||
|
|
||||||
|
$this->storage = $this->entityManager->getStorage($entity_type_id);
|
||||||
|
|
||||||
|
foreach ($this->getTestFiles('image') as $file) {
|
||||||
|
$entity = File::create((array) $file + ['status' => 1]);
|
||||||
|
$entity->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state->set('content_translation.entity_access.file', ['view' => TRUE]);
|
||||||
|
|
||||||
|
$account = User::create([
|
||||||
|
'name' => $this->randomMachineName(),
|
||||||
|
'status' => 1,
|
||||||
|
]);
|
||||||
|
$account->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that field synchronization works as expected with revisions.
|
||||||
|
*
|
||||||
|
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::create
|
||||||
|
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::validate
|
||||||
|
* @covers \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges
|
||||||
|
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::getFieldSynchronizedProperties
|
||||||
|
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeFields
|
||||||
|
* @covers \Drupal\content_translation\FieldTranslationSynchronizer::synchronizeItems
|
||||||
|
*/
|
||||||
|
public function testFieldSynchronizationAndValidation() {
|
||||||
|
// Test that when untranslatable field widgets are displayed, synchronized
|
||||||
|
// field properties can be changed only in default revisions.
|
||||||
|
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
|
||||||
|
$entity = $this->saveNewEntity();
|
||||||
|
$entity_id = $entity->id();
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [1, 1, 1, 'Alt 1 EN']);
|
||||||
|
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||||
|
$en_revision = $this->createRevision($entity, FALSE);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 2;
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertViolations($violations);
|
||||||
|
|
||||||
|
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||||
|
$it_revision = $this->createRevision($it_translation, FALSE);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||||
|
$metadata->setSource('en');
|
||||||
|
$it_revision->get($this->fieldName)->target_id = 2;
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertViolations($violations);
|
||||||
|
$it_revision->isDefaultRevision(TRUE);
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [2, 2, 2, 'Alt 1 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($en_revision, FALSE);
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [3, 2, 2, 'Alt 3 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision, FALSE);
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [4, 2, 2, 'Alt 1 EN', 'Alt 4 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($en_revision);
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 5 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [5, 2, 2, 'Alt 5 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($en_revision);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 6;
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [6, 6, 6, 'Alt 6 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision);
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [7, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
|
||||||
|
|
||||||
|
// Test that when untranslatable field widgets are hidden, synchronized
|
||||||
|
// field properties can be changed only when editing the default
|
||||||
|
// translation. This may lead to temporarily desynchronized values, when
|
||||||
|
// saving a pending revision for the default translation that changes a
|
||||||
|
// synchronized property (see revision 11).
|
||||||
|
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||||
|
$entity = $this->saveNewEntity();
|
||||||
|
$entity_id = $entity->id();
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [8, 1, 1, 'Alt 1 EN']);
|
||||||
|
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||||
|
$en_revision = $this->createRevision($entity, FALSE);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 2;
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 2 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [9, 2, 2, 'Alt 2 EN']);
|
||||||
|
|
||||||
|
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||||
|
$it_revision = $this->createRevision($it_translation, FALSE);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||||
|
$metadata->setSource('en');
|
||||||
|
$it_revision->get($this->fieldName)->target_id = 3;
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertViolations($violations);
|
||||||
|
$it_revision->isDefaultRevision(TRUE);
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertViolations($violations);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_translation);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||||
|
$metadata->setSource('en');
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 3 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [10, 1, 1, 'Alt 1 EN', 'Alt 3 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($en_revision, FALSE);
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [11, 2, 1, 'Alt 4 EN', 'Alt 3 IT']);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision, FALSE);
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 5 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [12, 1, 1, 'Alt 1 EN', 'Alt 5 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($en_revision);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 6;
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 6 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [13, 6, 6, 'Alt 6 EN', 'Alt 3 IT']);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision);
|
||||||
|
$it_revision->get($this->fieldName)->target_id = 7;
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertViolations($violations);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision);
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 7 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [14, 6, 6, 'Alt 6 EN', 'Alt 7 IT']);
|
||||||
|
|
||||||
|
// Test that creating a default revision starting from a pending revision
|
||||||
|
// having changes to synchronized properties, without introducing new
|
||||||
|
// changes works properly.
|
||||||
|
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||||
|
$entity = $this->saveNewEntity();
|
||||||
|
$entity_id = $entity->id();
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [15, 1, 1, 'Alt 1 EN']);
|
||||||
|
|
||||||
|
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||||
|
$it_revision = $this->createRevision($it_translation);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||||
|
$metadata->setSource('en');
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [16, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||||
|
$en_revision = $this->createRevision($entity);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 3;
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [17, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($entity, FALSE);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 4;
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 4 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [18, 4, 3, 'Alt 4 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($entity);
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [19, 4, 4, 'Alt 4 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision);
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 6 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [20, 4, 4, 'Alt 4 EN', 'Alt 6 IT']);
|
||||||
|
|
||||||
|
// Check that we are not allowed to perform changes to multiple translations
|
||||||
|
// in pending revisions when synchronized properties are involved.
|
||||||
|
$this->setUntranslatableFieldWidgetsDisplay(FALSE);
|
||||||
|
$entity = $this->saveNewEntity();
|
||||||
|
$entity_id = $entity->id();
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [21, 1, 1, 'Alt 1 EN']);
|
||||||
|
|
||||||
|
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||||
|
$it_revision = $this->createRevision($it_translation);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||||
|
$metadata->setSource('en');
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [22, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$en_revision = $this->createRevision($entity, FALSE);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 2;
|
||||||
|
$en_revision->getTranslation('it')->get($this->fieldName)->alt = 'Alt 3 IT';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertViolations($violations);
|
||||||
|
|
||||||
|
// Test that when saving a new default revision starting from a pending
|
||||||
|
// revision, outdated synchronized properties do not override more recent
|
||||||
|
// ones.
|
||||||
|
$this->setUntranslatableFieldWidgetsDisplay(TRUE);
|
||||||
|
$entity = $this->saveNewEntity();
|
||||||
|
$entity_id = $entity->id();
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [23, 1, 1, 'Alt 1 EN']);
|
||||||
|
|
||||||
|
$it_translation = $entity->addTranslation('it', $entity->toArray());
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $it_revision */
|
||||||
|
$it_revision = $this->createRevision($it_translation, FALSE);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($it_revision);
|
||||||
|
$metadata->setSource('en');
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 2 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [24, 1, 1, 'Alt 1 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $en_revision */
|
||||||
|
$en_revision = $this->createRevision($entity);
|
||||||
|
$en_revision->get($this->fieldName)->target_id = 3;
|
||||||
|
$en_revision->get($this->fieldName)->alt = 'Alt 3 EN';
|
||||||
|
$violations = $en_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($en_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [25, 3, 3, 'Alt 3 EN', 'Alt 2 IT']);
|
||||||
|
|
||||||
|
$it_revision = $this->createRevision($it_revision);
|
||||||
|
$it_revision->get($this->fieldName)->alt = 'Alt 4 IT';
|
||||||
|
$violations = $it_revision->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($it_revision);
|
||||||
|
$this->assertLatestRevisionFieldValues($entity_id, [26, 3, 3, 'Alt 3 EN', 'Alt 4 IT']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets untranslatable field widgets' display status.
|
||||||
|
*
|
||||||
|
* @param bool $display
|
||||||
|
* Whether untranslatable field widgets should be displayed.
|
||||||
|
*/
|
||||||
|
protected function setUntranslatableFieldWidgetsDisplay($display) {
|
||||||
|
$entity_type_id = $this->storage->getEntityTypeId();
|
||||||
|
$settings = ['untranslatable_fields_hide' => !$display];
|
||||||
|
$this->contentTranslationManager->setBundleTranslationSettings($entity_type_id, $entity_type_id, $settings);
|
||||||
|
/** @var \Drupal\Core\Entity\EntityTypeBundleInfo $bundle_info */
|
||||||
|
$bundle_info = $this->container->get('entity_type.bundle.info');
|
||||||
|
$bundle_info->clearCachedBundles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||||
|
*/
|
||||||
|
protected function saveNewEntity() {
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||||
|
$entity = EntityTestMulRev::create([
|
||||||
|
'uid' => 1,
|
||||||
|
'langcode' => 'en',
|
||||||
|
$this->fieldName => [
|
||||||
|
'target_id' => 1,
|
||||||
|
'alt' => 'Alt 1 EN',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
|
||||||
|
$metadata->setSource(LanguageInterface::LANGCODE_NOT_SPECIFIED);
|
||||||
|
$violations = $entity->validate();
|
||||||
|
$this->assertEmpty($violations);
|
||||||
|
$this->storage->save($entity);
|
||||||
|
return $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new revision starting from the latest translation-affecting one.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $translation
|
||||||
|
* The translation to be revisioned.
|
||||||
|
* @param bool $default
|
||||||
|
* (optional) Whether the new revision should be marked as default. Defaults
|
||||||
|
* to TRUE.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||||
|
* An entity revision object.
|
||||||
|
*/
|
||||||
|
protected function createRevision(ContentEntityInterface $translation, $default = TRUE) {
|
||||||
|
if (!$translation->isNewTranslation()) {
|
||||||
|
$langcode = $translation->language()->getId();
|
||||||
|
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($translation->id(), $langcode);
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||||
|
$revision = $this->storage->loadRevision($revision_id);
|
||||||
|
$translation = $revision->getTranslation($langcode);
|
||||||
|
}
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||||
|
$revision = $this->storage->createRevision($translation, $default);
|
||||||
|
return $revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the expected violations were found.
|
||||||
|
*
|
||||||
|
* @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
|
||||||
|
* A list of violations.
|
||||||
|
*/
|
||||||
|
protected function assertViolations(EntityConstraintViolationListInterface $violations) {
|
||||||
|
$entity_type_id = $this->storage->getEntityTypeId();
|
||||||
|
$settings = $this->contentTranslationManager->getBundleTranslationSettings($entity_type_id, $entity_type_id);
|
||||||
|
$message = !empty($settings['untranslatable_fields_hide']) ?
|
||||||
|
'Non-translatable field elements can only be changed when updating the original language.' :
|
||||||
|
'Non-translatable field elements can only be changed when updating the current revision.';
|
||||||
|
|
||||||
|
$list = [];
|
||||||
|
foreach ($violations as $violation) {
|
||||||
|
if ((string) $violation->getMessage() === $message) {
|
||||||
|
$list[] = $violation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertCount(1, $list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the latest revision has the expected field values.
|
||||||
|
*
|
||||||
|
* @param $entity_id
|
||||||
|
* The entity ID.
|
||||||
|
* @param array $expected_values
|
||||||
|
* An array of expected values in the following order:
|
||||||
|
* - revision ID
|
||||||
|
* - target ID (en)
|
||||||
|
* - target ID (it)
|
||||||
|
* - alt (en)
|
||||||
|
* - alt (it)
|
||||||
|
*/
|
||||||
|
protected function assertLatestRevisionFieldValues($entity_id, array $expected_values) {
|
||||||
|
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||||
|
$entity = $this->storage->loadRevision($this->storage->getLatestRevisionId($entity_id));
|
||||||
|
@list($revision_id, $target_id_en, $target_id_it, $alt_en, $alt_it) = $expected_values;
|
||||||
|
$this->assertEquals($revision_id, $entity->getRevisionId());
|
||||||
|
$this->assertEquals($target_id_en, $entity->get($this->fieldName)->target_id);
|
||||||
|
$this->assertEquals($alt_en, $entity->get($this->fieldName)->alt);
|
||||||
|
if ($entity->hasTranslation('it')) {
|
||||||
|
$it_translation = $entity->getTranslation('it');
|
||||||
|
$this->assertEquals($target_id_it, $it_translation->get($this->fieldName)->target_id);
|
||||||
|
$this->assertEquals($alt_it, $it_translation->get($this->fieldName)->alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -59,7 +59,7 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
|
||||||
protected function setUp() {
|
protected function setUp() {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'));
|
$this->synchronizer = new FieldTranslationSynchronizer($this->container->get('entity.manager'), $this->container->get('plugin.manager.field.field_type'));
|
||||||
$this->synchronized = ['sync1', 'sync2'];
|
$this->synchronized = ['sync1', 'sync2'];
|
||||||
$this->columns = array_merge($this->synchronized, ['var1', 'var2']);
|
$this->columns = array_merge($this->synchronized, ['var1', 'var2']);
|
||||||
$this->langcodes = ['en', 'it', 'fr', 'de', 'es'];
|
$this->langcodes = ['en', 'it', 'fr', 'de', 'es'];
|
||||||
|
|
Loading…
Reference in New Issue