updated core from 8.4 to 8.5 : bug with login_destination
This commit is contained in:
@@ -18,3 +18,9 @@ language.content_settings.*.*.third_party.content_translation:
|
||||
enabled:
|
||||
type: boolean
|
||||
label: 'Content translation enabled'
|
||||
bundle_settings:
|
||||
type: sequence
|
||||
label: 'Content translation bundle settings'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Bundle settings values'
|
||||
|
||||
@@ -17,11 +17,10 @@
|
||||
const $context = $(context);
|
||||
const options = drupalSettings.contentTranslationDependentOptions;
|
||||
let $fields;
|
||||
let dependent_columns;
|
||||
|
||||
function fieldsChangeHandler($fields, dependent_columns) {
|
||||
function fieldsChangeHandler($fields, dependentColumns) {
|
||||
return function (e) {
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns, $(e.target));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns, $(e.target));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,18 +28,16 @@
|
||||
// that name and copy over the input values that require all columns to be
|
||||
// translatable.
|
||||
if (options && options.dependent_selectors) {
|
||||
for (const field in options.dependent_selectors) {
|
||||
if (options.dependent_selectors.hasOwnProperty(field)) {
|
||||
$fields = $context.find(`input[name^="${field}"]`);
|
||||
dependent_columns = options.dependent_selectors[field];
|
||||
Object.keys(options.dependent_selectors).forEach((field) => {
|
||||
$fields = $context.find(`input[name^="${field}"]`);
|
||||
const dependentColumns = options.dependent_selectors[field];
|
||||
|
||||
$fields.on('change', fieldsChangeHandler($fields, dependent_columns));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns);
|
||||
}
|
||||
}
|
||||
$fields.on('change', fieldsChangeHandler($fields, dependentColumns));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns);
|
||||
});
|
||||
}
|
||||
},
|
||||
check($fields, dependent_columns, $changed) {
|
||||
check($fields, dependentColumns, $changed) {
|
||||
let $element = $changed;
|
||||
let column;
|
||||
|
||||
@@ -50,23 +47,21 @@
|
||||
|
||||
// A field that has many different translatable parts can also define one
|
||||
// or more columns that require all columns to be translatable.
|
||||
for (const index in dependent_columns) {
|
||||
if (dependent_columns.hasOwnProperty(index)) {
|
||||
column = dependent_columns[index];
|
||||
Object.keys(dependentColumns || {}).forEach((index) => {
|
||||
column = dependentColumns[index];
|
||||
|
||||
if (!$changed) {
|
||||
$element = $fields.filter(filterFieldsList);
|
||||
}
|
||||
|
||||
if ($element.is(`input[value="${column}"]:checked`)) {
|
||||
$fields.prop('checked', true)
|
||||
.not($element).prop('disabled', true);
|
||||
}
|
||||
else {
|
||||
$fields.prop('disabled', false);
|
||||
}
|
||||
if (!$changed) {
|
||||
$element = $fields.filter(filterFieldsList);
|
||||
}
|
||||
}
|
||||
|
||||
if ($element.is(`input[value="${column}"]:checked`)) {
|
||||
$fields.prop('checked', true)
|
||||
.not($element).prop('disabled', true);
|
||||
}
|
||||
else {
|
||||
$fields.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,7 +84,12 @@
|
||||
$bundleSettings.nextUntil('.bundle-settings').hide();
|
||||
}
|
||||
else {
|
||||
$bundleSettings.nextUntil('.bundle-settings', '.field-settings').find('.translatable :input:not(:checked)').closest('.field-settings').nextUntil(':not(.column-settings)').hide();
|
||||
$bundleSettings
|
||||
.nextUntil('.bundle-settings', '.field-settings')
|
||||
.find('.translatable :input:not(:checked)')
|
||||
.closest('.field-settings')
|
||||
.nextUntil(':not(.column-settings)')
|
||||
.hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* The content translation administration forms.
|
||||
*/
|
||||
|
||||
use Drupal\content_translation\BundleTranslationSettingsInterface;
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
@@ -83,6 +85,7 @@ function _content_translation_form_language_content_settings_form_alter(array &$
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
|
||||
$content_translation_manager = \Drupal::service('content_translation.manager');
|
||||
$default = $form['entity_types']['#default_value'];
|
||||
foreach ($default as $entity_type_id => $enabled) {
|
||||
@@ -110,6 +113,26 @@ function _content_translation_form_language_content_settings_form_alter(array &$
|
||||
continue;
|
||||
}
|
||||
|
||||
// Displayed the "shared fields widgets" toggle.
|
||||
if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
|
||||
$settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
|
||||
$force_hidden = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
|
||||
$form['settings'][$entity_type_id][$bundle]['settings']['content_translation']['untranslatable_fields_hide'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Hide non translatable fields on translation forms'),
|
||||
'#default_value' => $force_hidden || !empty($settings['untranslatable_fields_hide']),
|
||||
'#disabled' => $force_hidden,
|
||||
'#description' => $force_hidden ? t('Moderated content requires non-translatable fields to be edited in the original language form.') : '',
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name="settings[' . $entity_type_id . '][' . $bundle . '][translatable]"]' => [
|
||||
'checked' => TRUE,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$fields = $entity_manager->getFieldDefinitions($entity_type_id, $bundle);
|
||||
if ($fields) {
|
||||
foreach ($fields as $field_name => $definition) {
|
||||
@@ -317,6 +340,8 @@ function content_translation_form_language_content_settings_validate(array $form
|
||||
* @see content_translation_admin_settings_form_validate()
|
||||
*/
|
||||
function content_translation_form_language_content_settings_submit(array $form, FormStateInterface $form_state) {
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
|
||||
$content_translation_manager = \Drupal::service('content_translation.manager');
|
||||
$entity_types = $form_state->getValue('entity_types');
|
||||
$settings = &$form_state->getValue('settings');
|
||||
|
||||
@@ -347,7 +372,12 @@ function content_translation_form_language_content_settings_submit(array $form,
|
||||
}
|
||||
if (isset($bundle_settings['translatable'])) {
|
||||
// Store whether a bundle has translation enabled or not.
|
||||
\Drupal::service('content_translation.manager')->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
|
||||
$content_translation_manager->setEnabled($entity_type_id, $bundle, $bundle_settings['translatable']);
|
||||
|
||||
// Store any other bundle settings.
|
||||
if ($content_translation_manager instanceof BundleTranslationSettingsInterface) {
|
||||
$content_translation_manager->setBundleTranslationSettings($entity_type_id, $bundle, $bundle_settings['settings']['content_translation']);
|
||||
}
|
||||
|
||||
// Save translation_sync settings.
|
||||
if (!empty($bundle_settings['columns'])) {
|
||||
@@ -367,8 +397,8 @@ function content_translation_form_language_content_settings_submit(array $form,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure entity and menu router information are correctly rebuilt.
|
||||
\Drupal::entityManager()->clearCachedDefinitions();
|
||||
\Drupal::service('router.builder')->setRebuildNeeded();
|
||||
|
||||
}
|
||||
|
||||
@@ -11,27 +11,24 @@
|
||||
var $context = $(context);
|
||||
var options = drupalSettings.contentTranslationDependentOptions;
|
||||
var $fields = void 0;
|
||||
var dependent_columns = void 0;
|
||||
|
||||
function fieldsChangeHandler($fields, dependent_columns) {
|
||||
function fieldsChangeHandler($fields, dependentColumns) {
|
||||
return function (e) {
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns, $(e.target));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns, $(e.target));
|
||||
};
|
||||
}
|
||||
|
||||
if (options && options.dependent_selectors) {
|
||||
for (var field in options.dependent_selectors) {
|
||||
if (options.dependent_selectors.hasOwnProperty(field)) {
|
||||
$fields = $context.find('input[name^="' + field + '"]');
|
||||
dependent_columns = options.dependent_selectors[field];
|
||||
Object.keys(options.dependent_selectors).forEach(function (field) {
|
||||
$fields = $context.find('input[name^="' + field + '"]');
|
||||
var dependentColumns = options.dependent_selectors[field];
|
||||
|
||||
$fields.on('change', fieldsChangeHandler($fields, dependent_columns));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependent_columns);
|
||||
}
|
||||
}
|
||||
$fields.on('change', fieldsChangeHandler($fields, dependentColumns));
|
||||
Drupal.behaviors.contentTranslationDependentOptions.check($fields, dependentColumns);
|
||||
});
|
||||
}
|
||||
},
|
||||
check: function check($fields, dependent_columns, $changed) {
|
||||
check: function check($fields, dependentColumns, $changed) {
|
||||
var $element = $changed;
|
||||
var column = void 0;
|
||||
|
||||
@@ -39,21 +36,19 @@
|
||||
return $(field).val() === column;
|
||||
}
|
||||
|
||||
for (var index in dependent_columns) {
|
||||
if (dependent_columns.hasOwnProperty(index)) {
|
||||
column = dependent_columns[index];
|
||||
Object.keys(dependentColumns || {}).forEach(function (index) {
|
||||
column = dependentColumns[index];
|
||||
|
||||
if (!$changed) {
|
||||
$element = $fields.filter(filterFieldsList);
|
||||
}
|
||||
|
||||
if ($element.is('input[value="' + column + '"]:checked')) {
|
||||
$fields.prop('checked', true).not($element).prop('disabled', true);
|
||||
} else {
|
||||
$fields.prop('disabled', false);
|
||||
}
|
||||
if (!$changed) {
|
||||
$element = $fields.filter(filterFieldsList);
|
||||
}
|
||||
}
|
||||
|
||||
if ($element.is('input[value="' + column + '"]:checked')) {
|
||||
$fields.prop('checked', true).not($element).prop('disabled', true);
|
||||
} else {
|
||||
$fields.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ package: Multilingual
|
||||
# core: 8.x
|
||||
configure: language.content_settings_page
|
||||
|
||||
# Information added by Drupal.org packaging script on 2018-01-03
|
||||
version: '8.4.4'
|
||||
# Information added by Drupal.org packaging script on 2018-03-07
|
||||
version: '8.5.0'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1515021228
|
||||
datestamp: 1520457826
|
||||
|
||||
@@ -59,6 +59,7 @@ function content_translation_update_8400() {
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
|
||||
$entity_type_manager->clearCachedDefinitions();
|
||||
foreach ($content_translation_manager->getSupportedEntityTypes() as $entity_type_id => $entity_type_definition) {
|
||||
$storage = $entity_type_manager->getStorage($entity_type_id);
|
||||
if ($storage instanceof SqlEntityStorageInterface) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* Allows entities to be translated into different languages.
|
||||
*/
|
||||
|
||||
use Drupal\content_translation\BundleTranslationSettingsInterface;
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityFormInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
@@ -59,6 +61,14 @@ function content_translation_module_implements_alter(&$implementations, $hook) {
|
||||
unset($implementations['content_translation']);
|
||||
$implementations['content_translation'] = $group;
|
||||
break;
|
||||
|
||||
// Move our hook_entity_bundle_info_alter() implementation to the top of the
|
||||
// list, so that any other hook implementation can rely on bundles being
|
||||
// correctly marked as translatable.
|
||||
case 'entity_bundle_info_alter':
|
||||
$group = $implementations['content_translation'];
|
||||
$implementations = ['content_translation' => $group] + $implementations;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +164,8 @@ function content_translation_entity_type_alter(array &$entity_types) {
|
||||
}
|
||||
$entity_type->set('translation', $translation);
|
||||
}
|
||||
|
||||
$entity_type->addConstraint('ContentTranslationSynchronizedFields');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +173,19 @@ function content_translation_entity_type_alter(array &$entity_types) {
|
||||
* Implements hook_entity_bundle_info_alter().
|
||||
*/
|
||||
function content_translation_entity_bundle_info_alter(&$bundles) {
|
||||
foreach ($bundles as $entity_type => &$info) {
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */
|
||||
$content_translation_manager = \Drupal::service('content_translation.manager');
|
||||
foreach ($bundles as $entity_type_id => &$info) {
|
||||
foreach ($info as $bundle => &$bundle_info) {
|
||||
$bundle_info['translatable'] = \Drupal::service('content_translation.manager')->isEnabled($entity_type, $bundle);
|
||||
$bundle_info['translatable'] = $content_translation_manager->isEnabled($entity_type_id, $bundle);
|
||||
if ($bundle_info['translatable'] && $content_translation_manager instanceof BundleTranslationSettingsInterface) {
|
||||
$settings = $content_translation_manager->getBundleTranslationSettings($entity_type_id, $bundle);
|
||||
// If pending revision support is enabled for this bundle, we need to
|
||||
// hide untranslatable field widgets, otherwise changes in pending
|
||||
// revisions might be overridden by changes in later default revisions.
|
||||
$bundle_info['untranslatable_fields.default_translation_affected'] =
|
||||
!empty($settings['untranslatable_fields_hide']) || ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,8 +205,8 @@ function content_translation_entity_base_field_info(EntityTypeInterface $entity_
|
||||
// when translation is disabled.
|
||||
// @todo Re-evaluate this approach and consider removing field storage
|
||||
// definitions and the related field data if the entity type has no bundle
|
||||
// enabled for translation, once base field purging is supported.
|
||||
// See https://www.drupal.org/node/2282119.
|
||||
// enabled for translation.
|
||||
// @see https://www.drupal.org/node/2907777
|
||||
if ($manager->isEnabled($entity_type_id) || array_intersect_key($definitions, $installed_storage_definitions)) {
|
||||
return $definitions;
|
||||
}
|
||||
@@ -319,6 +341,11 @@ function content_translation_form_alter(array &$form, FormStateInterface $form_s
|
||||
}
|
||||
}
|
||||
|
||||
// The footer region, if defined, may contain multilingual widgets so we
|
||||
// need to always display it.
|
||||
if (isset($form['footer'])) {
|
||||
$form['footer']['#multilingual'] = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +437,11 @@ function content_translation_form_field_config_edit_form_alter(array &$form, For
|
||||
*/
|
||||
function content_translation_entity_presave(EntityInterface $entity) {
|
||||
if ($entity instanceof ContentEntityInterface && $entity->isTranslatable() && !$entity->isNew()) {
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
|
||||
$manager = \Drupal::service('content_translation.manager');
|
||||
if (!$manager->isEnabled($entity->getEntityTypeId(), $entity->bundle())) {
|
||||
return;
|
||||
}
|
||||
// If we are creating a new translation we need to use the source language
|
||||
// as original language, since source values are the only ones available to
|
||||
// compare against.
|
||||
@@ -418,8 +450,6 @@ function content_translation_entity_presave(EntityInterface $entity) {
|
||||
->getStorage($entity->entityType())->loadUnchanged($entity->id());
|
||||
}
|
||||
$langcode = $entity->language()->getId();
|
||||
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $manager */
|
||||
$manager = \Drupal::service('content_translation.manager');
|
||||
$source_langcode = !$entity->original->hasTranslation($langcode) ? $manager->getTranslationMetadata($entity)->getSource() : NULL;
|
||||
\Drupal::service('content_translation.synchronizer')->synchronizeFields($entity, $langcode, $source_langcode);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
content_translation.synchronizer:
|
||||
class: Drupal\content_translation\FieldTranslationSynchronizer
|
||||
arguments: ['@entity.manager']
|
||||
arguments: ['@entity.manager', '@plugin.manager.field.field_type']
|
||||
|
||||
content_translation.subscriber:
|
||||
class: Drupal\content_translation\Routing\ContentTranslationRouteSubscriber
|
||||
@@ -9,6 +9,12 @@ services:
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
||||
content_translation.delete_access:
|
||||
class: Drupal\content_translation\Access\ContentTranslationDeleteAccess
|
||||
arguments: ['@entity_type.manager', '@content_translation.manager']
|
||||
tags:
|
||||
- { name: access_check, applies_to: _access_content_translation_delete }
|
||||
|
||||
content_translation.overview_access:
|
||||
class: Drupal\content_translation\Access\ContentTranslationOverviewAccess
|
||||
arguments: ['@entity.manager']
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ id: d6_taxonomy_term_translation
|
||||
label: Taxonomy terms
|
||||
migration_tags:
|
||||
- Drupal 6
|
||||
- Content
|
||||
source:
|
||||
plugin: d6_taxonomy_term
|
||||
translations: true
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation\Access;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Access check for entity translation deletion.
|
||||
*
|
||||
* @internal This additional access checker only aims to prevent deletions in
|
||||
* pending revisions until we are able to flag revision translations as
|
||||
* deleted.
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/node/2945956.
|
||||
*/
|
||||
class ContentTranslationDeleteAccess implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* Constructs a ContentTranslationDeleteAccess object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
|
||||
* The content translation manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $manager, ContentTranslationManagerInterface $content_translation_manager) {
|
||||
$this->entityTypeManager = $manager;
|
||||
$this->contentTranslationManager = $content_translation_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access to translation deletion for the specified route match.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parameterized route.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
|
||||
$requirement = $route_match->getRouteObject()->getRequirement('_access_content_translation_delete');
|
||||
$entity_type_id = current(explode('.', $requirement));
|
||||
$entity = $route_match->getParameter($entity_type_id);
|
||||
return $this->checkAccess($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access to translation deletion for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity translation to be deleted.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function checkAccess(ContentEntityInterface $entity) {
|
||||
$result = AccessResult::allowed();
|
||||
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$result->addCacheableDependency($entity);
|
||||
// Add the cache dependencies used by
|
||||
// ContentTranslationManager::isPendingRevisionSupportEnabled().
|
||||
if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
|
||||
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
|
||||
$result->addCacheableDependency($workflow);
|
||||
}
|
||||
}
|
||||
if (!ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($entity->isDefaultTranslation()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$config = ContentLanguageSettings::load($entity_type_id . '.' . $entity->bundle());
|
||||
$result->addCacheableDependency($config);
|
||||
if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
$revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId());
|
||||
if (!$revision_id) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $storage->loadRevision($revision_id);
|
||||
if ($revision->wasDefaultRevision()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result = $result->andIf(AccessResult::forbidden());
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
+38
-12
@@ -3,6 +3,7 @@
|
||||
namespace Drupal\content_translation\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
@@ -90,15 +91,12 @@ class ContentTranslationManageAccessCheck implements AccessInterface {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
|
||||
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
|
||||
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
|
||||
// Load translation.
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
|
||||
switch ($operation) {
|
||||
case 'create':
|
||||
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
|
||||
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
$source_language = $this->languageManager->getLanguage($source) ?: $entity->language();
|
||||
$target_language = $this->languageManager->getLanguage($target) ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
|
||||
$is_new_translation = ($source_language->getId() != $target_language->getId()
|
||||
@@ -109,12 +107,14 @@ class ContentTranslationManageAccessCheck implements AccessInterface {
|
||||
->andIf($handler->getTranslationAccess($entity, $operation));
|
||||
|
||||
case 'delete':
|
||||
// @todo Remove this in https://www.drupal.org/node/2945956.
|
||||
/** @var \Drupal\Core\Access\AccessResultInterface $delete_access */
|
||||
$delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity);
|
||||
$access = $this->checkAccess($entity, $language, $operation);
|
||||
return $delete_access->andIf($access);
|
||||
|
||||
case 'update':
|
||||
$has_translation = isset($languages[$language->getId()])
|
||||
&& $language->getId() != $entity->getUntranslated()->language()->getId()
|
||||
&& isset($translations[$language->getId()]);
|
||||
return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity)
|
||||
->andIf($handler->getTranslationAccess($entity, $operation));
|
||||
return $this->checkAccess($entity, $language, $operation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,4 +122,30 @@ class ContentTranslationManageAccessCheck implements AccessInterface {
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs access checks for the specified operation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being checked.
|
||||
* @param \Drupal\Core\Language\LanguageInterface $language
|
||||
* For an update or delete operation, the language code of the translation
|
||||
* being updated or deleted.
|
||||
* @param string $operation
|
||||
* The operation to be checked.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* An access result object.
|
||||
*/
|
||||
protected function checkAccess(ContentEntityInterface $entity, LanguageInterface $language, $operation) {
|
||||
/* @var \Drupal\content_translation\ContentTranslationHandlerInterface $handler */
|
||||
$handler = $this->entityManager->getHandler($entity->getEntityTypeId(), 'translation');
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
$languages = $this->languageManager->getLanguages();
|
||||
$has_translation = isset($languages[$language->getId()])
|
||||
&& $language->getId() != $entity->getUntranslated()->language()->getId()
|
||||
&& isset($translations[$language->getId()]);
|
||||
return AccessResult::allowedIf($has_translation)->cachePerPermissions()->addCacheableDependency($entity)
|
||||
->andIf($handler->getTranslationAccess($entity, $operation));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
/**
|
||||
* Interface providing support for content translation bundle settings.
|
||||
*/
|
||||
interface BundleTranslationSettingsInterface {
|
||||
|
||||
/**
|
||||
* Returns translation settings for the specified bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param string $bundle
|
||||
* The bundle name.
|
||||
*
|
||||
* @return array
|
||||
* An associative array of values keyed by setting name.
|
||||
*/
|
||||
public function getBundleTranslationSettings($entity_type_id, $bundle);
|
||||
|
||||
/**
|
||||
* Sets translation settings for the specified bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param string $bundle
|
||||
* The bundle name.
|
||||
* @param array $settings
|
||||
* An associative array of values keyed by setting name.
|
||||
*/
|
||||
public function setBundleTranslationSettings($entity_type_id, $bundle, array $settings);
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Drupal\content_translation;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
|
||||
use Drupal\Core\Entity\EntityChangedInterface;
|
||||
use Drupal\Core\Entity\EntityChangesDetectionTrait;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
@@ -13,8 +14,10 @@ use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
@@ -25,7 +28,10 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
* @ingroup entity_api
|
||||
*/
|
||||
class ContentTranslationHandler implements ContentTranslationHandlerInterface, EntityHandlerInterface {
|
||||
|
||||
use EntityChangesDetectionTrait;
|
||||
use DependencySerializationTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The type of the entity being translated.
|
||||
@@ -55,6 +61,13 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
@@ -70,6 +83,13 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
*/
|
||||
protected $fieldStorageDefinitions;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* Initializes an instance of the content translation controller.
|
||||
*
|
||||
@@ -83,14 +103,18 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
* The entity manager.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user) {
|
||||
public function __construct(EntityTypeInterface $entity_type, LanguageManagerInterface $language_manager, ContentTranslationManagerInterface $manager, EntityManagerInterface $entity_manager, AccountInterface $current_user, MessengerInterface $messenger) {
|
||||
$this->entityTypeId = $entity_type->id();
|
||||
$this->entityType = $entity_type;
|
||||
$this->languageManager = $language_manager;
|
||||
$this->manager = $manager;
|
||||
$this->entityTypeManager = $entity_manager;
|
||||
$this->currentUser = $current_user;
|
||||
$this->fieldStorageDefinitions = $entity_manager->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +126,8 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
$container->get('language_manager'),
|
||||
$container->get('content_translation.manager'),
|
||||
$container->get('entity.manager'),
|
||||
$container->get('current_user')
|
||||
$container->get('current_user'),
|
||||
$container->get('messenger')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -269,6 +294,8 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
|
||||
$form_object = $form_state->getFormObject();
|
||||
$form_langcode = $form_object->getFormLangcode($form_state);
|
||||
$entity_langcode = $entity->getUntranslated()->language()->getId();
|
||||
@@ -363,7 +390,12 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
break;
|
||||
}
|
||||
}
|
||||
$access = $this->getTranslationAccess($entity, 'delete')->isAllowed() || ($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form'));
|
||||
/** @var \Drupal\Core\Access\AccessResultInterface $delete_access */
|
||||
$delete_access = \Drupal::service('content_translation.delete_access')->checkAccess($entity);
|
||||
$access = $delete_access->isAllowed() && (
|
||||
$this->getTranslationAccess($entity, 'delete')->isAllowed() ||
|
||||
($entity->access('delete') && $this->entityType->hasLinkTemplate('delete-form'))
|
||||
);
|
||||
$form['actions']['delete_translation'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Delete translation'),
|
||||
@@ -426,12 +458,19 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
];
|
||||
|
||||
$translate = !$new_translation && $metadata->isOutdated();
|
||||
if (!$translate) {
|
||||
$outdated_access = !ContentTranslationManager::isPendingRevisionSupportEnabled($entity->getEntityTypeId(), $entity->bundle());
|
||||
if (!$outdated_access) {
|
||||
$form['content_translation']['outdated'] = [
|
||||
'#markup' => $this->t('Translations cannot be flagged as outdated when content is moderated.'),
|
||||
];
|
||||
}
|
||||
elseif (!$translate) {
|
||||
$form['content_translation']['retranslate'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Flag other translations as outdated'),
|
||||
'#default_value' => FALSE,
|
||||
'#description' => t('If you made a significant change, which means the other translations should be updated, you can flag all translations of this content as outdated. This will not change any other property of them, like whether they are published or not.'),
|
||||
'#access' => $outdated_access,
|
||||
];
|
||||
}
|
||||
else {
|
||||
@@ -440,6 +479,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
'#title' => t('This translation needs to be updated'),
|
||||
'#default_value' => $translate,
|
||||
'#description' => t('When this option is checked, this translation needs to be updated. Uncheck when the translation is up to date again.'),
|
||||
'#access' => $outdated_access,
|
||||
];
|
||||
$form['content_translation']['#open'] = TRUE;
|
||||
}
|
||||
@@ -512,6 +552,20 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
$ignored_types = array_flip(['actions', 'value', 'hidden', 'vertical_tabs', 'token', 'details']);
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
|
||||
$form_object = $form_state->getFormObject();
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $form_object->getEntity();
|
||||
$display_translatability_clue = !$entity->isDefaultTranslationAffectedOnly();
|
||||
$hide_untranslatable_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation();
|
||||
$translation_form = $form_state->get(['content_translation', 'translation_form']);
|
||||
$display_warning = FALSE;
|
||||
|
||||
// We use field definitions to identify untranslatable field widgets to be
|
||||
// hidden. Fields that are not involved in translation changes checks should
|
||||
// not be affected by this logic (the "revision_log" field, for instance).
|
||||
$field_definitions = array_diff_key($entity->getFieldDefinitions(), array_flip($this->getFieldsToSkipFromTranslationChangesCheck($entity)));
|
||||
|
||||
foreach (Element::children($element) as $key) {
|
||||
if (!isset($element[$key]['#type'])) {
|
||||
$this->entityFormSharedElements($element[$key], $form_state, $form);
|
||||
@@ -524,10 +578,17 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
// Elements are considered to be non multilingual by default.
|
||||
if (empty($element[$key]['#multilingual'])) {
|
||||
// If we are displaying a multilingual entity form we need to provide
|
||||
// translatability clues, otherwise the shared form elements should be
|
||||
// hidden.
|
||||
if (!$form_state->get(['content_translation', 'translation_form'])) {
|
||||
$this->addTranslatabilityClue($element[$key]);
|
||||
// translatability clues, otherwise the non-multilingual form elements
|
||||
// should be hidden.
|
||||
if (!$translation_form) {
|
||||
if ($display_translatability_clue) {
|
||||
$this->addTranslatabilityClue($element[$key]);
|
||||
}
|
||||
// Hide widgets for untranslatable fields.
|
||||
if ($hide_untranslatable_fields && isset($field_definitions[$key])) {
|
||||
$element[$key]['#access'] = FALSE;
|
||||
$display_warning = TRUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$element[$key]['#access'] = FALSE;
|
||||
@@ -536,6 +597,11 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
}
|
||||
}
|
||||
|
||||
if ($display_warning && !$form_state->isSubmitted() && !$form_state->isRebuilding()) {
|
||||
$url = $entity->getUntranslated()->toUrl('edit-form')->toString();
|
||||
$this->messenger->addWarning($this->t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.', [':url' => $url]));
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
@@ -643,7 +709,7 @@ class ContentTranslationHandler implements ContentTranslationHandlerInterface, E
|
||||
// after the entity has been validated, so that it does not break the
|
||||
// EntityChanged constraint validator. The content translation metadata
|
||||
// field for the changed timestamp does not have such a constraint defined
|
||||
// at the moment, but it is correct to update it's value in a submission
|
||||
// at the moment, but it is correct to update its value in a submission
|
||||
// handler as well and have the same logic like in the Form API.
|
||||
if ($entity->hasField('content_translation_changed')) {
|
||||
$metadata = $this->manager->getTranslationMetadata($entity);
|
||||
|
||||
@@ -4,11 +4,12 @@ namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Provides common functionality for content translation.
|
||||
*/
|
||||
class ContentTranslationManager implements ContentTranslationManagerInterface {
|
||||
class ContentTranslationManager implements ContentTranslationManagerInterface, BundleTranslationSettingsInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
@@ -105,6 +106,23 @@ class ContentTranslationManager implements ContentTranslationManagerInterface {
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setBundleTranslationSettings($entity_type_id, $bundle, array $settings) {
|
||||
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
|
||||
$config->setThirdPartySetting('content_translation', 'bundle_settings', $settings)
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getBundleTranslationSettings($entity_type_id, $bundle) {
|
||||
$config = $this->loadContentLanguageSettings($entity_type_id, $bundle);
|
||||
return $config->getThirdPartySetting('content_translation', 'bundle_settings', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a content language config entity based on the entity type and bundle.
|
||||
*
|
||||
@@ -128,4 +146,47 @@ class ContentTranslationManager implements ContentTranslationManagerInterface {
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether support for pending revisions should be enabled.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The ID of the entity type to be checked.
|
||||
* @param string $bundle_id
|
||||
* (optional) The ID of the bundle to be checked. Defaults to none.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if pending revisions should be enabled, FALSE otherwise.
|
||||
*
|
||||
* @internal
|
||||
* There is ongoing discussion about how pending revisions should behave.
|
||||
* The logic enabling pending revision support is likely to change once a
|
||||
* decision is made.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2940575
|
||||
*/
|
||||
public static function isPendingRevisionSupportEnabled($entity_type_id, $bundle_id = NULL) {
|
||||
if (!\Drupal::moduleHandler()->moduleExists('content_moderation')) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
|
||||
$plugin = $workflow->getTypePlugin();
|
||||
$entity_type_ids = array_flip($plugin->getEntityTypes());
|
||||
if (isset($entity_type_ids[$entity_type_id])) {
|
||||
if (!isset($bundle_id)) {
|
||||
return TRUE;
|
||||
}
|
||||
else {
|
||||
$bundle_ids = array_flip($plugin->getBundlesForEntityType($entity_type_id));
|
||||
if (isset($bundle_ids[$bundle_id])) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Drupal\content_translation\Controller;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
@@ -87,6 +88,7 @@ class ContentTranslationController extends ControllerBase {
|
||||
$handler = $this->entityManager()->getHandler($entity_type_id, 'translation');
|
||||
$manager = $this->manager;
|
||||
$entity_type = $entity->getEntityType();
|
||||
$use_latest_revisions = $entity_type->isRevisionable() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle());
|
||||
|
||||
// Start collecting the cacheability metadata, starting with the entity and
|
||||
// later merge in the access result cacheability metadata.
|
||||
@@ -99,6 +101,9 @@ class ContentTranslationController extends ControllerBase {
|
||||
|
||||
$rows = [];
|
||||
$show_source_column = FALSE;
|
||||
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager()->getStorage($entity_type_id);
|
||||
$default_revision = $storage->load($entity->id());
|
||||
|
||||
if ($this->languageManager()->isMultilingual()) {
|
||||
// Determine whether the current entity is translatable.
|
||||
@@ -121,6 +126,25 @@ class ContentTranslationController extends ControllerBase {
|
||||
$language_name = $language->getName();
|
||||
$langcode = $language->getId();
|
||||
|
||||
// If the entity type is revisionable, we may have pending revisions
|
||||
// with translations not available yet in the default revision. Thus we
|
||||
// need to load the latest translation-affecting revision for each
|
||||
// language to be sure we are listing all available translations.
|
||||
if ($use_latest_revisions) {
|
||||
$entity = $default_revision;
|
||||
$latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
|
||||
if ($latest_revision_id) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
|
||||
$latest_revision = $storage->loadRevision($latest_revision_id);
|
||||
// Make sure we do not list removed translations, i.e. translations
|
||||
// that have been part of a default revision but no longer are.
|
||||
if (!$latest_revision->wasDefaultRevision() || $default_revision->hasTranslation($langcode)) {
|
||||
$entity = $latest_revision;
|
||||
}
|
||||
}
|
||||
$translations = $entity->getTranslationLanguages();
|
||||
}
|
||||
|
||||
$add_url = new Url(
|
||||
"entity.$entity_type_id.content_translation_add",
|
||||
[
|
||||
@@ -212,24 +236,34 @@ class ContentTranslationController extends ControllerBase {
|
||||
$source_name = $this->t('n/a');
|
||||
}
|
||||
else {
|
||||
$source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
|
||||
$delete_access = $entity->access('delete', NULL, TRUE);
|
||||
$translation_access = $handler->getTranslationAccess($entity, 'delete');
|
||||
$cacheability = $cacheability
|
||||
->merge(CacheableMetadata::createFromObject($delete_access))
|
||||
->merge(CacheableMetadata::createFromObject($translation_access));
|
||||
if ($entity->access('delete') && $entity_type->hasLinkTemplate('delete-form')) {
|
||||
$links['delete'] = [
|
||||
'title' => $this->t('Delete'),
|
||||
'url' => $entity->urlInfo('delete-form'),
|
||||
'language' => $language,
|
||||
];
|
||||
/** @var \Drupal\Core\Access\AccessResultInterface $delete_route_access */
|
||||
$delete_route_access = \Drupal::service('content_translation.delete_access')->checkAccess($translation);
|
||||
$cacheability->addCacheableDependency($delete_route_access);
|
||||
|
||||
if ($delete_route_access->isAllowed()) {
|
||||
$source_name = isset($languages[$source]) ? $languages[$source]->getName() : $this->t('n/a');
|
||||
$delete_access = $entity->access('delete', NULL, TRUE);
|
||||
$translation_access = $handler->getTranslationAccess($entity, 'delete');
|
||||
$cacheability
|
||||
->addCacheableDependency($delete_access)
|
||||
->addCacheableDependency($translation_access);
|
||||
|
||||
if ($delete_access->isAllowed() && $entity_type->hasLinkTemplate('delete-form')) {
|
||||
$links['delete'] = [
|
||||
'title' => $this->t('Delete'),
|
||||
'url' => $entity->urlInfo('delete-form'),
|
||||
'language' => $language,
|
||||
];
|
||||
}
|
||||
elseif ($translation_access->isAllowed()) {
|
||||
$links['delete'] = [
|
||||
'title' => $this->t('Delete'),
|
||||
'url' => $delete_url,
|
||||
];
|
||||
}
|
||||
}
|
||||
elseif ($translation_access->isAllowed()) {
|
||||
$links['delete'] = [
|
||||
'title' => $this->t('Delete'),
|
||||
'url' => $delete_url,
|
||||
];
|
||||
else {
|
||||
$this->messenger()->addWarning($this->t('The "Delete translation" action is only available for published translations.'), FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,8 +364,21 @@ class ContentTranslationController extends ControllerBase {
|
||||
* A processed form array ready to be rendered.
|
||||
*/
|
||||
public function add(LanguageInterface $source, LanguageInterface $target, RouteMatchInterface $route_match, $entity_type_id = NULL) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $route_match->getParameter($entity_type_id);
|
||||
|
||||
// In case of a pending revision, make sure we load the latest
|
||||
// translation-affecting revision for the source language, otherwise the
|
||||
// initial form values may not be up-to-date.
|
||||
if (!$entity->isDefaultRevision() && ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id, $entity->bundle())) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager()->getStorage($entity->getEntityTypeId());
|
||||
$revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $source->getId());
|
||||
if ($revision_id != $entity->getRevisionId()) {
|
||||
$entity = $storage->loadRevision($revision_id);
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Exploit the upcoming hook_entity_prepare() when available.
|
||||
// See https://www.drupal.org/node/1810394.
|
||||
$this->prepareTranslation($entity, $source, $target);
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace Drupal\content_translation;
|
||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides field translation synchronization capabilities.
|
||||
@@ -18,14 +20,57 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* The field type plugin manager.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
|
||||
*/
|
||||
protected $fieldTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a FieldTranslationSynchronizer object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
|
||||
* 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->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) {
|
||||
$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
|
||||
// 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.
|
||||
$entity_type = $entity->getEntityTypeId();
|
||||
$entity_unchanged = isset($entity->original) ? $entity->original : $this->entityManager->getStorage($entity_type)->loadUnchanged($entity->id());
|
||||
$entity_unchanged = $this->getOriginalEntity($entity);
|
||||
if ($entity->getUntranslated()->language()->getId() != $entity_unchanged->getUntranslated()->language()->getId()) {
|
||||
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 */
|
||||
foreach ($entity as $field_name => $items) {
|
||||
$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'];
|
||||
|
||||
// Sync if the field is translatable, not empty, and the synchronization
|
||||
// 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
|
||||
// single list.
|
||||
$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}
|
||||
*/
|
||||
@@ -174,9 +272,7 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||
// items and the other columns from the existing values. This only
|
||||
// works if the delta exists in the language.
|
||||
elseif ($created && !empty($original_field_values[$langcode][$delta])) {
|
||||
$item_columns_to_sync = array_intersect_key($source_items[$delta], array_flip($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;
|
||||
$values[$langcode][$delta] = $this->createMergedItem($source_items[$delta], $original_field_values[$langcode][$delta], $columns);
|
||||
}
|
||||
// If the delta doesn't exist, copy from the source language.
|
||||
elseif ($created) {
|
||||
@@ -190,13 +286,37 @@ class FieldTranslationSynchronizer implements FieldTranslationSynchronizerInterf
|
||||
// If the value has only been reordered we just move the old one in
|
||||
// the new position.
|
||||
$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.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Drupal\content_translation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ use Drupal\Core\Language\LanguageInterface;
|
||||
|
||||
/**
|
||||
* Delete translation form for content_translation module.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ContentTranslationDeleteForm extends ContentEntityDeleteForm {
|
||||
|
||||
|
||||
+25
@@ -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.';
|
||||
|
||||
}
|
||||
+226
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Drupal\content_translation\Routing;
|
||||
|
||||
use Drupal\content_translation\ContentTranslationManager;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||
use Drupal\Core\Routing\RoutingEvents;
|
||||
@@ -55,6 +56,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
|
||||
}
|
||||
|
||||
$path = $base_path . '/translations';
|
||||
$load_latest_revision = ContentTranslationManager::isPendingRevisionSupportEnabled($entity_type_id);
|
||||
|
||||
$route = new Route(
|
||||
$path,
|
||||
@@ -70,6 +72,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
|
||||
'parameters' => [
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
@@ -102,6 +105,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
|
||||
],
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
@@ -127,6 +131,7 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
|
||||
],
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
@@ -152,12 +157,21 @@ class ContentTranslationRouteSubscriber extends RouteSubscriberBase {
|
||||
],
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_latest_revision' => $load_latest_revision,
|
||||
],
|
||||
],
|
||||
'_admin_route' => $is_admin,
|
||||
]
|
||||
);
|
||||
$collection->add("entity.$entity_type_id.content_translation_delete", $route);
|
||||
|
||||
// Add our custom translation deletion access checker.
|
||||
if ($load_latest_revision) {
|
||||
$entity_delete_route = $collection->get("entity.$entity_type_id.delete_form");
|
||||
if ($entity_delete_route) {
|
||||
$entity_delete_route->addRequirements(['_access_content_translation_delete' => "$entity_type_id.delete"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@ dependencies:
|
||||
- language
|
||||
- entity_test
|
||||
|
||||
# Information added by Drupal.org packaging script on 2018-01-03
|
||||
version: '8.4.4'
|
||||
# Information added by Drupal.org packaging script on 2018-03-07
|
||||
version: '8.5.0'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1515021228
|
||||
datestamp: 1520457826
|
||||
|
||||
+32
@@ -5,7 +5,39 @@
|
||||
* 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\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_entity_bundle_info_alter().
|
||||
*/
|
||||
function content_translation_test_entity_bundle_info_alter(&$bundles) {
|
||||
// Store the initial status of the "translatable" property for the
|
||||
// "entity_test_mul" bundle.
|
||||
$translatable = !empty($bundles['entity_test_mul']['entity_test_mul']['translatable']);
|
||||
\Drupal::state()->set('content_translation_test.translatable', $translatable);
|
||||
// Make it translatable if Content Translation did not. This will make the
|
||||
// entity object translatable even if it is disabled in Content Translation
|
||||
// settings.
|
||||
if (!$translatable) {
|
||||
$bundles['entity_test_mul']['entity_test_mul']['translatable'] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ dependencies:
|
||||
- content_translation
|
||||
- views
|
||||
|
||||
# Information added by Drupal.org packaging script on 2018-01-03
|
||||
version: '8.4.4'
|
||||
# Information added by Drupal.org packaging script on 2018-03-07
|
||||
version: '8.5.0'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1515021228
|
||||
datestamp: 1520457826
|
||||
|
||||
+6
-1
@@ -3,7 +3,8 @@
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Tests\NodeTestBase;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests the content translation language that is set.
|
||||
@@ -12,6 +13,10 @@ use Drupal\node\Tests\NodeTestBase;
|
||||
*/
|
||||
class ContentTranslationLanguageChangeTest extends NodeTestBase {
|
||||
|
||||
use TestFileCreationTrait {
|
||||
getTestFiles as drupalGetTestFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests the "Flag as outdated" functionality with revision translations.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationOutdatedRevisionTranslationTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->enableContentModeration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that outdated revision translations work correctly.
|
||||
*/
|
||||
public function testFlagAsOutdatedHidden() {
|
||||
// Create a test node.
|
||||
$values = [
|
||||
'title' => 'Test 1.1 EN',
|
||||
'moderation_state' => 'published',
|
||||
];
|
||||
$id = $this->createEntity($values, 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->load($id);
|
||||
|
||||
// Add a published Italian translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$this->assertFlagWidget();
|
||||
$edit = [
|
||||
'title[0][value]' => 'Test 1.2 IT',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
|
||||
// Add a published French translation.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'fr',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('fr'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$this->drupalGet($add_translation_url);
|
||||
$this->assertFlagWidget();
|
||||
$edit = [
|
||||
'title[0][value]' => 'Test 1.3 FR',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
|
||||
// Create an English draft.
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$en_edit_url = $this->getEditUrl($entity);
|
||||
$this->drupalGet($en_edit_url);
|
||||
$this->assertFlagWidget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the flag widget is displayed.
|
||||
*/
|
||||
protected function assertFlagWidget() {
|
||||
$this->assertSession()->pageTextNotContains('Flag other translations as outdated');
|
||||
$this->assertSession()->pageTextContains('Translations cannot be flagged as outdated when content is moderated.');
|
||||
}
|
||||
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* Base class for pending revision translation tests.
|
||||
*/
|
||||
abstract class ContentTranslationPendingRevisionTestBase extends ContentTranslationTestBase {
|
||||
|
||||
use ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['language', 'content_translation', 'content_moderation', 'node'];
|
||||
|
||||
/**
|
||||
* The entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Permissions common to all test accounts.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $commonPermissions;
|
||||
|
||||
/**
|
||||
* The current test account.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentAccount;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->entityTypeId = 'node';
|
||||
$this->bundle = 'article';
|
||||
|
||||
$this->commonPermissions = [
|
||||
'view any unpublished content',
|
||||
"translate {$this->bundle} {$this->entityTypeId}",
|
||||
"create content translations",
|
||||
'use editorial transition create_new_draft',
|
||||
'use editorial transition publish',
|
||||
'use editorial transition archive',
|
||||
'use editorial transition archived_draft',
|
||||
'use editorial transition archived_published',
|
||||
];
|
||||
|
||||
parent::setUp();
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
|
||||
$entity_type_manager = $this->container->get('entity_type.manager');
|
||||
$this->storage = $entity_type_manager->getStorage($this->entityTypeId);
|
||||
|
||||
// @todo Remove this line once https://www.drupal.org/node/2945928 is fixed.
|
||||
$this->config('node.settings')->set('use_admin_theme', '1')->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables content moderation for the test entity type and bundle.
|
||||
*/
|
||||
protected function enableContentModeration() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
$workflow_id = 'editorial';
|
||||
$this->drupalGet('/admin/config/workflow/workflows');
|
||||
$edit['bundles[' . $this->bundle . ']'] = TRUE;
|
||||
$this->drupalPostForm('admin/config/workflow/workflows/manage/' . $workflow_id . '/type/' . $this->entityTypeId, $edit, t('Save'));
|
||||
// Ensure the parent environment is up-to-date.
|
||||
// @see content_moderation_workflow_insert()
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
/** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
|
||||
$router_builder = $this->container->get('router.builder');
|
||||
$router_builder->rebuildIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditorPermissions() {
|
||||
$editor_permissions = [
|
||||
"edit any {$this->bundle} content",
|
||||
"delete any {$this->bundle} content",
|
||||
"view {$this->bundle} revisions",
|
||||
"delete {$this->bundle} revisions",
|
||||
];
|
||||
return array_merge($editor_permissions, $this->commonPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTranslatorPermissions() {
|
||||
return array_merge(parent::getTranslatorPermissions(), $this->commonPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setupBundle() {
|
||||
parent::setupBundle();
|
||||
$this->createContentType(['type' => $this->bundle]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the active revision translation for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
* @param string $langcode
|
||||
* The translation language code.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface|null
|
||||
* The active revision translation or NULL if none could be identified.
|
||||
*/
|
||||
protected function loadRevisionTranslation(ContentEntityInterface $entity, $langcode) {
|
||||
$revision_id = $this->storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
|
||||
$revision = $revision_id ? $this->storage->loadRevision($revision_id) : NULL;
|
||||
return $revision && $revision->hasTranslation($langcode) ? $revision->getTranslation($langcode) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the edit URL for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The edit URL.
|
||||
*/
|
||||
protected function getEditUrl(ContentEntityInterface $entity) {
|
||||
if ($entity->access('update', $this->loggedInUser)) {
|
||||
$url = $entity->toUrl('edit-form');
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('drupal:content-translation-edit');
|
||||
$url->setRouteParameter('language', $entity->language()->getId());
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the delete translation URL for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The delete translation URL.
|
||||
*/
|
||||
protected function getDeleteUrl(ContentEntityInterface $entity) {
|
||||
if ($entity->access('delete', $this->loggedInUser)) {
|
||||
$url = $entity->toUrl('delete-form');
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('drupal:content-translation-delete');
|
||||
$url->setRouteParameter('language', $entity->language()->getId());
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests that revision translation deletion is handled correctly.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationRevisionTranslationDeletionTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->enableContentModeration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that translation overview handles pending revisions correctly.
|
||||
*/
|
||||
public function testOverview() {
|
||||
$index = 1;
|
||||
$accounts = [
|
||||
$this->rootUser,
|
||||
$this->editor,
|
||||
$this->translator,
|
||||
];
|
||||
foreach ($accounts as $account) {
|
||||
$this->currentAccount = $account;
|
||||
$this->doTestOverview($index++);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a test run.
|
||||
*
|
||||
* @param int $index
|
||||
* The test run index.
|
||||
*/
|
||||
public function doTestOverview($index) {
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
|
||||
// Create a test node.
|
||||
$values = [
|
||||
'title' => "Test $index.1 EN",
|
||||
'moderation_state' => 'published',
|
||||
];
|
||||
$id = $this->createEntity($values, 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->storage->load($id);
|
||||
|
||||
// Add a draft translation and check that it is available only in the latest
|
||||
// revision.
|
||||
$add_translation_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $id,
|
||||
'source' => 'en',
|
||||
'target' => 'it',
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$add_translation_href = $add_translation_url->toString();
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.2 IT",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
|
||||
// Check that translations cannot be deleted in drafts.
|
||||
$overview_url = $entity->toUrl('drupal:content-translation-overview');
|
||||
$this->drupalGet($overview_url);
|
||||
$it_delete_url = $this->getDeleteUrl($it_revision);
|
||||
$it_delete_href = $it_delete_url->toString();
|
||||
$this->assertSession()->linkByHrefNotExists($it_delete_href);
|
||||
$warning = 'The "Delete translation" action is only available for published translations.';
|
||||
$this->assertSession()->pageTextContains($warning);
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$this->assertSession()->buttonNotExists('Delete translation');
|
||||
|
||||
// Publish the translation and verify it can be deleted.
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.3 IT",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefExists($it_delete_href);
|
||||
$this->assertSession()->pageTextNotContains($warning);
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$this->assertSession()->buttonExists('Delete translation');
|
||||
|
||||
// Create an English draft and verify the published translation was
|
||||
// preserved.
|
||||
$this->drupalLogin($this->editor);
|
||||
$en_revision = $this->loadRevisionTranslation($entity, 'en');
|
||||
$this->drupalGet($this->getEditUrl($en_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.4 EN",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$en_revision = $this->loadRevisionTranslation($entity, 'en');
|
||||
$this->assertTrue($en_revision->hasTranslation('it'));
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
|
||||
// Delete the translation and verify that it is actually gone and that it is
|
||||
// possible to create it again.
|
||||
$this->drupalGet($it_delete_url);
|
||||
$this->drupalPostForm(NULL, [], 'Delete Italian translation');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->wasDefaultRevision());
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->assertTrue($it_revision->getRevisionId() < $entity->getRevisionId());
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
|
||||
$this->assertSession()->linkByHrefExists($add_translation_href);
|
||||
|
||||
// Publish the English draft and verify the translation is not accidentally
|
||||
// restored.
|
||||
$this->drupalLogin($this->editor);
|
||||
$en_revision = $this->loadRevisionTranslation($entity, 'en');
|
||||
$this->drupalGet($this->getEditUrl($en_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.6 EN",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
|
||||
// Create a published translation again and verify it could be deleted.
|
||||
$this->drupalGet($add_translation_url);
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.7 IT",
|
||||
'moderation_state[0][state]' => 'published',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefExists($it_delete_href);
|
||||
|
||||
// Create a translation draft again and verify it cannot be deleted.
|
||||
$this->drupalGet($this->getEditUrl($it_revision));
|
||||
$edit = [
|
||||
'title[0][value]' => "Test $index.8 IT",
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertTrue($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefNotExists($it_delete_href);
|
||||
|
||||
// Delete the translation draft and verify the translation can be deleted
|
||||
// again, since the active revision is now a default revision.
|
||||
$this->drupalLogin($this->editor);
|
||||
$this->drupalGet($it_revision->toUrl('version-history'));
|
||||
$revision_deletion_url = Url::fromRoute('node.revision_delete_confirm', [
|
||||
'node' => $id,
|
||||
'node_revision' => $it_revision->getRevisionId(),
|
||||
],
|
||||
[
|
||||
'language' => ConfigurableLanguage::load('it'),
|
||||
'absolute' => FALSE,
|
||||
]
|
||||
);
|
||||
$revision_deletion_href = $revision_deletion_url->toString();
|
||||
$this->getSession()->getDriver()->click("//a[@href='$revision_deletion_href']");
|
||||
$this->drupalPostForm(NULL, [], 'Delete');
|
||||
$this->drupalLogin($this->currentAccount);
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefExists($it_delete_href);
|
||||
|
||||
// Verify that now the translation can be deleted.
|
||||
$this->drupalGet($it_delete_url);
|
||||
$this->drupalPostForm(NULL, [], 'Delete Italian translation');
|
||||
$entity = $this->storage->loadUnchanged($id);
|
||||
$this->assertFalse($entity->hasTranslation('it'));
|
||||
$it_revision = $this->loadRevisionTranslation($entity, 'it');
|
||||
$this->assertTrue($it_revision->wasDefaultRevision());
|
||||
$this->assertTrue($it_revision->hasTranslation('it'));
|
||||
$this->assertTrue($it_revision->getRevisionId() < $entity->getRevisionId());
|
||||
$this->drupalGet($overview_url);
|
||||
$this->assertSession()->linkByHrefNotExists($this->getEditUrl($it_revision)->toString());
|
||||
$this->assertSession()->linkByHrefExists($add_translation_href);
|
||||
}
|
||||
|
||||
}
|
||||
+22
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
@@ -138,7 +139,7 @@ abstract class ContentTranslationTestBase extends BrowserTestBase {
|
||||
* Returns an array of permissions needed for the administrator.
|
||||
*/
|
||||
protected function getAdministratorPermissions() {
|
||||
return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer content translation']);
|
||||
return array_merge($this->getEditorPermissions(), $this->getTranslatorPermissions(), ['administer languages', 'administer content translation']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,4 +237,24 @@ abstract class ContentTranslationTestBase extends BrowserTestBase {
|
||||
return $entity->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the edit URL for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity being edited.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The edit URL.
|
||||
*/
|
||||
protected function getEditUrl(ContentEntityInterface $entity) {
|
||||
if ($entity->access('update', $this->loggedInUser)) {
|
||||
$url = $entity->toUrl('edit-form');
|
||||
}
|
||||
else {
|
||||
$url = $entity->toUrl('drupal:content-translation-edit');
|
||||
$url->setRouteParameter('language', $entity->language()->getId());
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_translation\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests the untranslatable fields behaviors.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationUntranslatableFieldsTest extends ContentTranslationPendingRevisionTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Configure one field as untranslatable.
|
||||
$this->drupalLogin($this->administrator);
|
||||
$edit = [
|
||||
'settings[' . $this->entityTypeId . '][' . $this->bundle . '][fields][' . $this->fieldName . ']' => 0,
|
||||
];
|
||||
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
|
||||
$entity_field_manager = $this->container->get('entity_field.manager');
|
||||
$entity_field_manager->clearCachedFieldDefinitions();
|
||||
$definitions = $entity_field_manager->getFieldDefinitions($this->entityTypeId, $this->bundle);
|
||||
$this->assertFalse($definitions[$this->fieldName]->isTranslatable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that hiding untranslatable field widgets works correctly.
|
||||
*/
|
||||
public function testHiddenWidgets() {
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
|
||||
$entity_type_manager = $this->container->get('entity_type.manager');
|
||||
$id = $this->createEntity(['title' => $this->randomString()], 'en');
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $entity_type_manager
|
||||
->getStorage($this->entityTypeId)
|
||||
->load($id);
|
||||
|
||||
// Check that the untranslatable field widget is displayed on the edit form
|
||||
// and no translatability clue is displayed yet.
|
||||
$en_edit_url = $entity->toUrl('edit-form');
|
||||
$this->drupalGet($en_edit_url);
|
||||
$field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$clue_xpath = '//label[@for="edit-' . strtr($this->fieldName, '_', '-') . '-0-value"]/span[text()="(all languages)"]';
|
||||
$this->assertEmpty($this->xpath($clue_xpath));
|
||||
|
||||
// Add a translation and check that the untranslatable field widget is
|
||||
// displayed on the translation and edit forms along with translatability
|
||||
// clues.
|
||||
$add_url = Url::fromRoute("entity.{$this->entityTypeId}.content_translation_add", [
|
||||
$entity->getEntityTypeId() => $entity->id(),
|
||||
'source' => 'en',
|
||||
'target' => 'it'
|
||||
]);
|
||||
$this->drupalGet($add_url);
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$this->assertNotEmpty($this->xpath($clue_xpath));
|
||||
$this->drupalPostForm(NULL, [], 'Save');
|
||||
|
||||
// Check that the widget is displayed along with its clue in the edit form
|
||||
// for both languages.
|
||||
$this->drupalGet($en_edit_url);
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$this->assertNotEmpty($this->xpath($clue_xpath));
|
||||
$it_edit_url = $entity->toUrl('edit-form', ['language' => ConfigurableLanguage::load('it')]);
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$this->assertNotEmpty($this->xpath($clue_xpath));
|
||||
|
||||
// Configure untranslatable field widgets to be hidden on non-default
|
||||
// language edit forms.
|
||||
$settings_key = 'settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]';
|
||||
$settings_url = 'admin/config/regional/content-language';
|
||||
$this->drupalPostForm($settings_url, [$settings_key => 1], 'Save configuration');
|
||||
|
||||
// Verify that the widget is displayed in the default language edit form,
|
||||
// but no clue is displayed.
|
||||
$this->drupalGet($en_edit_url);
|
||||
$field_xpath = '//input[@name="' . $this->fieldName . '[0][value]"]';
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$this->assertEmpty($this->xpath($clue_xpath));
|
||||
|
||||
// Verify no widget is displayed on the non-default language edit form.
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertEmpty($this->xpath($field_xpath));
|
||||
$this->assertEmpty($this->xpath($clue_xpath));
|
||||
|
||||
// Verify a warning is displayed.
|
||||
$this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
|
||||
$edit_path = $entity->toUrl('edit-form')->toString();
|
||||
$link_xpath = '//a[@href=:edit_path and text()="Edit them on the original language form"]';
|
||||
$elements = $this->xpath($link_xpath, [':edit_path' => $edit_path]);
|
||||
$this->assertNotEmpty($elements);
|
||||
|
||||
// Configure untranslatable field widgets to be displayed on non-default
|
||||
// language edit forms.
|
||||
$this->drupalPostForm($settings_url, [$settings_key => 0], 'Save configuration');
|
||||
|
||||
// Check that the widget is displayed along with its clue in the edit form
|
||||
// for both languages.
|
||||
$this->drupalGet($en_edit_url);
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$this->assertNotEmpty($this->xpath($clue_xpath));
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertNotEmpty($this->xpath($field_xpath));
|
||||
$this->assertNotEmpty($this->xpath($clue_xpath));
|
||||
|
||||
// Enable content moderation and verify that widgets are hidden despite them
|
||||
// being configured to be displayed.
|
||||
$this->enableContentModeration();
|
||||
$this->drupalGet($it_edit_url);
|
||||
$this->assertEmpty($this->xpath($field_xpath));
|
||||
$this->assertEmpty($this->xpath($clue_xpath));
|
||||
|
||||
// Verify a warning is displayed.
|
||||
$this->assertSession()->pageTextContains('Fields that apply to all languages are hidden to avoid conflicting changes.');
|
||||
$elements = $this->xpath($link_xpath, [':edit_path' => $edit_path]);
|
||||
$this->assertNotEmpty($elements);
|
||||
|
||||
// Verify that checkboxes on the language content settings page are checked
|
||||
// and disabled for moderated bundles.
|
||||
$this->drupalGet($settings_url);
|
||||
$input_xpath = '//input[@name="settings[' . $this->entityTypeId . '][' . $this->bundle . '][settings][content_translation][untranslatable_fields_hide]" and @value=1 and @disabled="disabled"]';
|
||||
$elements = $this->xpath($input_xpath);
|
||||
$this->assertNotEmpty($elements);
|
||||
$this->drupalPostForm(NULL, [$settings_key => 0], 'Save configuration');
|
||||
$elements = $this->xpath($input_xpath);
|
||||
$this->assertNotEmpty($elements);
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_translation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\entity_test\Entity\EntityTestMul;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests the Content Translation bundle info logic.
|
||||
*
|
||||
* @group content_translation
|
||||
*/
|
||||
class ContentTranslationEntityBundleInfoTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['user', 'language', 'content_translation_test', 'content_translation', 'entity_test'];
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*
|
||||
* @var \Drupal\content_translation\ContentTranslationManagerInterface
|
||||
*/
|
||||
protected $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* The bundle info service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfo
|
||||
*/
|
||||
protected $bundleInfo;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->contentTranslationManager = $this->container->get('content_translation.manager');
|
||||
$this->bundleInfo = $this->container->get('entity_type.bundle.info');
|
||||
|
||||
$this->installEntitySchema('entity_test_mul');
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('it')->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that modules can know whether bundles are translatable.
|
||||
*/
|
||||
public function testHookInvocationOrder() {
|
||||
$this->contentTranslationManager->setEnabled('entity_test_mul', 'entity_test_mul', TRUE);
|
||||
$this->bundleInfo->clearCachedBundles();
|
||||
$this->bundleInfo->getAllBundleInfo();
|
||||
|
||||
// Verify that the test module comes first in the module list, which would
|
||||
// normally make its hook implementation to be invoked first.
|
||||
/** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
|
||||
$module_handler = $this->container->get('module_handler');
|
||||
$module_list = $module_handler->getModuleList();
|
||||
$expected_modules = [
|
||||
'content_translation_test',
|
||||
'content_translation',
|
||||
];
|
||||
$actual_modules = array_keys(array_intersect_key($module_list, array_flip($expected_modules)));
|
||||
$this->assertEquals($expected_modules, $actual_modules);
|
||||
|
||||
// Check that the "content_translation_test" hook implementation has access
|
||||
// to the "translatable" bundle info property.
|
||||
/** @var \Drupal\Core\State\StateInterface $state */
|
||||
$state = $this->container->get('state');
|
||||
$this->assertTrue($state->get('content_translation_test.translatable'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that field synchronization is skipped for disabled bundles.
|
||||
*/
|
||||
public function testFieldSynchronizationWithDisabledBundle() {
|
||||
$entity = EntityTestMul::create();
|
||||
$entity->save();
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $translation */
|
||||
$translation = $entity->addTranslation('it');
|
||||
$translation->save();
|
||||
|
||||
$this->assertTrue($entity->isTranslatable());
|
||||
}
|
||||
|
||||
}
|
||||
+482
@@ -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', 'simpletest', '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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -59,7 +59,7 @@ class ContentTranslationSyncUnitTest extends KernelTestBase {
|
||||
protected function 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->columns = array_merge($this->synchronized, ['var1', 'var2']);
|
||||
$this->langcodes = ['en', 'it', 'fr', 'de', 'es'];
|
||||
|
||||
Reference in New Issue
Block a user