Commit 17df2c5c authored by alexpott's avatar alexpott

Issue #1916790 by plach, YesCT, penyaskito, Gábor Hojtsy, das-peter, herom,...

Issue #1916790 by plach, YesCT, penyaskito, Gábor Hojtsy, das-peter, herom, larowlan: Convert translation metadata into regular entity fields
parent c15cf7d2
......@@ -158,11 +158,9 @@ public function testDisabledBundle() {
$enabled_block_content = $this->createBlockContent();
$disabled_block_content = $this->createBlockContent(FALSE, $bundle->id());
// Make sure that only a single row was inserted into the
// {content_translation} table.
$rows = db_query('SELECT * FROM {content_translation}')->fetchAll();
// Make sure that only a single row was inserted into the block table.
$rows = db_query('SELECT * FROM {block_content_field_data} WHERE id = :id', array(':id' => $enabled_block_content->id()))->fetchAll();
$this->assertEqual(1, count($rows));
$this->assertEqual($enabled_block_content->id(), reset($rows)->entity_id);
}
}
......@@ -9,12 +9,28 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\content_translation\ContentTranslationHandler;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines the translation handler for comments.
*/
class CommentTranslationHandler extends ContentTranslationHandler {
/**
* {@inheritdoc}
*/
public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
parent::entityFormAlter($form, $form_state, $entity);
if (isset($form['content_translation'])) {
// We do not need to show these values on comment forms: they inherit the
// basic comment property values.
$form['content_translation']['status']['#access'] = FALSE;
$form['content_translation']['name']['#access'] = FALSE;
$form['content_translation']['created']['#access'] = FALSE;
}
}
/**
* {@inheritdoc}
*/
......@@ -22,4 +38,17 @@ protected function entityFormTitle(EntityInterface $entity) {
return t('Edit comment @subject', array('@subject' => $entity->label()));
}
/**
* {@inheritdoc}
*/
public function entityFormEntityBuild($entity_type, EntityInterface $entity, array $form, FormStateInterface $form_state) {
if ($form_state->hasValue('content_translation')) {
$translation = &$form_state->getValue('content_translation');
/** @var \Drupal\comment\CommentInterface $entity */
$translation['status'] = $entity->isPublished();
$translation['name'] = $entity->getAuthorName();
}
parent::entityFormEntityBuild($entity_type, $entity, $form, $form_state);
}
}
......@@ -9,6 +9,7 @@
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\content_translation\Tests\ContentTranslationUITest;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the Comment Translation UI.
......@@ -108,30 +109,58 @@ protected function getNewEntityValues($langcode) {
}
/**
* Overrides \Drupal\content_translation\Tests\ContentTranslationUITest::assertPublishedStatus().
* {@inheritdoc}
*/
protected function assertPublishedStatus() {
parent::assertPublishedStatus();
$entity = entity_load($this->entityTypeId, $this->entityId);
$user = $this->drupalCreateUser(array('access comments'));
$this->drupalLogin($user);
$languages = $this->container->get('language_manager')->getLanguages();
protected function doTestPublishedStatus() {
$entity_manager = \Drupal::entityManager();
$storage = $entity_manager->getStorage($this->entityTypeId);
$storage->resetCache();
$entity = $storage->load($this->entityId);
// Check that simple users cannot see unpublished field translations.
// Unpublish translations.
foreach ($this->langcodes as $index => $langcode) {
$translation = $this->getTranslation($entity, $langcode);
$value = $this->getValue($translation, 'comment_body', $langcode);
$this->drupalGet($entity->urlInfo(), array('language' => $languages[$langcode]));
if ($index > 0) {
$this->assertNoRaw($value, 'Unpublished field translation is not shown.');
}
else {
$this->assertRaw($value, 'Published field translation is shown.');
$edit = array('status' => 0);
$url = $entity->urlInfo('edit-form', array('language' => ConfigurableLanguage::load($langcode)));
$this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode));
$storage->resetCache();
$entity = $storage->load($this->entityId);
$this->assertFalse($this->manager->getTranslationMetadata($entity->getTranslation($langcode))->isPublished(), 'The translation has been correctly unpublished.');
}
}
}
/**
* {@inheritdoc}
*/
protected function doTestAuthoringInfo() {
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
$path = $entity->getSystemPath('edit-form');
$languages = $this->container->get('language_manager')->getLanguages();
$values = array();
// Post different authoring information for each translation.
foreach ($this->langcodes as $langcode) {
$user = $this->drupalCreateUser();
$values[$langcode] = array(
'uid' => $user->id(),
'created' => REQUEST_TIME - mt_rand(0, 1000),
);
$edit = array(
'name' => $user->getUsername(),
'date[date]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d'),
'date[time]' => format_date($values[$langcode]['created'], 'custom', 'H:i:s'),
);
$this->drupalPostForm($path, $edit, $this->getFormSubmitAction($entity, $langcode), array('language' => $languages[$langcode]));
}
// Login as translator again to ensure subsequent tests do not break.
$this->drupalLogin($this->translator);
$entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
foreach ($this->langcodes as $langcode) {
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
$this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.');
$this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.');
}
}
/**
......
......@@ -97,7 +97,8 @@ function _content_translation_form_language_content_settings_form_alter(array &$
if ($fields) {
foreach ($fields as $field_name => $definition) {
// Allow to configure only fields supporting multilingual storage.
if (!empty($storage_definitions[$field_name]) && $storage_definitions[$field_name]->isTranslatable()) {
// We skip our own fields as they are always translatable.
if (!empty($storage_definitions[$field_name]) && $storage_definitions[$field_name]->isTranslatable() && $storage_definitions[$field_name]->getProvider() != 'content_translation') {
$form['settings'][$entity_type_id][$bundle]['fields'][$field_name] = array(
'#label' => $definition->getLabel(),
'#type' => 'checkbox',
......@@ -338,4 +339,5 @@ 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_indicator')->setRebuildNeeded();
\Drupal::service('content_translation.updates_manager')->updateDefinitions($entity_types);
}
......@@ -5,81 +5,9 @@
* Installation functions for Content Translation module.
*/
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
/**
* Implements hook_schema().
*/
function content_translation_schema() {
$schema['content_translation'] = array(
'description' => 'Table to track content translations',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => EntityTypeInterface::ID_MAX_LENGTH,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type this translation relates to',
),
'entity_id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The entity id this translation relates to',
),
'langcode' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The target language for this translation.',
),
'source' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The source language from which this translation was created.',
),
'outdated' => array(
'description' => 'A boolean indicating whether this translation needs to be updated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'uid' => array(
'description' => 'The author of this translation.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'status' => array(
'description' => 'Boolean indicating whether the translation is visible to non-translators.',
'type' => 'int',
'not null' => TRUE,
'default' => 1,
),
'created' => array(
'description' => 'The Unix timestamp when the translation was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the translation was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('entity_type', 'entity_id', 'langcode'),
);
return $schema;
}
/**
* Implements hook_install().
*/
......
......@@ -24,3 +24,9 @@ services:
content_translation.manager:
class: Drupal\content_translation\ContentTranslationManager
arguments: ['@entity.manager']
content_translation.updates_manager:
class: Drupal\content_translation\ContentTranslationUpdatesManager
arguments: ['@entity.manager', '@entity.definition_update_manager']
tags:
- { name: event_subscriber }
......@@ -18,6 +18,13 @@
*/
interface ContentTranslationHandlerInterface {
/**
* Returns a set of field definitions to be used to store metadata items.
*
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
*/
public function getFieldDefinitions();
/**
* Checks if the user can perform the given operation on translations of the
* wrapped entity.
......
......@@ -7,7 +7,9 @@
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\language\Entity\ContentLanguageSettings;
/**
......@@ -32,6 +34,23 @@ public function __construct(EntityManagerInterface $manager) {
$this->entityManager = $manager;
}
/**
* {@inheritdoc}
*/
function getTranslationHandler($entity_type_id) {
return $this->entityManager->getHandler($entity_type_id, 'translation');
}
/**
* {@inheritdoc}
*/
public function getTranslationMetadata(EntityInterface $translation) {
// We need a new instance of the metadata handler wrapping each translation.
$entity_type = $translation->getEntityType();
$class = $entity_type->get('content_translation_metadata');
return new $class($translation, $this->getTranslationHandler($entity_type->id()));
}
/**
* {@inheritdoc}
*/
......
......@@ -7,6 +7,9 @@
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Provides an interface for common functionality for content translation.
*/
......@@ -31,6 +34,28 @@ public function getSupportedEntityTypes();
*/
public function isSupported($entity_type_id);
/**
* Returns an instance of the Content translation handler.
*
* @param string $entity_type_id
* The type of the entity being translated.
*
* @return \Drupal\content_translation\ContentTranslationHandlerInterface
* An instance of the content translation handler.
*/
public function getTranslationHandler($entity_type_id);
/**
* Returns an instance of the Content translation metadata.
*
* @param \Drupal\Core\Entity\EntityInterface $translation
* The entity translation whose metadata needs to be retrieved.
*
* @return \Drupal\content_translation\ContentTranslationMetadataWrapperInterface
* An instance of the content translation metadata.
*/
public function getTranslationMetadata(EntityInterface $translation);
/**
* Sets the value for translatability of the given entity type bundle.
*
......
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationMetadata.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\UserInterface;
/**
* Base class for content translation metadata wrappers.
*/
class ContentTranslationMetadataWrapper implements ContentTranslationMetadataWrapperInterface {
/**
* The wrapped entity translation.
*
* @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\FieldableEntityInterface|\Drupal\Core\TypedData\TranslatableInterface
*/
protected $translation;
/**
* The content translation handler.
*
* @var \Drupal\content_translation\ContentTranslationHandlerInterface
*/
protected $handler;
/**
* Initializes an instance of the content translation metadata handler.
*
* @param EntityInterface $translation
* The entity translation to be wrapped.
* @param ContentTranslationHandlerInterface $handler
* The content translation handler.
*/
public function __construct(EntityInterface $translation, ContentTranslationHandlerInterface $handler) {
$this->translation = $translation;
$this->handler = $handler;
}
/**
* {@inheritdoc}
*/
public function getSource() {
return $this->translation->get('content_translation_source')->value;
}
/**
* {@inheritdoc}
*/
public function setSource($source) {
$this->translation->set('content_translation_source', $source);
return $this;
}
/**
* {@inheritdoc}
*/
public function isOutdated() {
return (bool) $this->translation->get('content_translation_outdated')->value;
}
/**
* {@inheritdoc}
*/
public function setOutdated($outdated) {
$this->translation->set('content_translation_outdated', $outdated);
return $this;
}
/**
* {@inheritdoc}
*/
public function getAuthor() {
return $this->translation->hasField('content_translation_uid') ? $this->translation->get('content_translation_uid')->entity : $this->translation->getOwner();
}
/**
* {@inheritdoc}
*/
public function setAuthor(UserInterface $account) {
if ($this->translation->hasField('content_translation_uid')) {
$this->translation->set('content_translation_uid', $account->id());
}
else {
$this->translation->setOwner($account);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function isPublished() {
$field_name = $this->translation->hasField('content_translation_status') ? 'content_translation_status' : 'status';
return (bool) $this->translation->get($field_name)->value;
}
/**
* {@inheritdoc}
*/
public function setPublished($published) {
$field_name = $this->translation->hasField('content_translation_status') ? 'content_translation_status' : 'status';
$this->translation->set($field_name, $published);
return $this;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
$field_name = $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
return $this->translation->get($field_name)->value;
}
/**
* {@inheritdoc}
*/
public function setCreatedTime($timestamp) {
$field_name = $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
$this->translation->set($field_name, $timestamp);
return $this;
}
/**
* {@inheritdoc}
*/
public function getChangedTime() {
return $this->translation->hasField('content_translation_changed') ? $this->translation->get('content_translation_changed')->value : $this->translation->getChangedTime();
}
/**
* {@inheritdoc}
*/
public function setChangedTime($timestamp) {
$field_name = $this->translation->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
$this->translation->set($field_name, $timestamp);
return $this;
}
}
<?php
/**
* @file
* Contains \Drupal\content_translation\ContentTranslationMetadataInterface.
*/
namespace Drupal\content_translation;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\user\UserInterface;
/**
* Common interface for content translation metadata wrappers.
*
* This acts as a wrapper for an entity translation object, encapsulating the
* logic needed to retrieve translation metadata.
*/
interface ContentTranslationMetadataWrapperInterface extends EntityChangedInterface {
/**
* Retrieves the source language for this translation.
*
* @return string
* The source language code.
*/
public function getSource();
/**
* Sets the source language for this translation.
*
* @param string $source
* The source language code.
*
* @return $this
*/
public function setSource($source);
/**
* Returns the translation outdated status.
*
* @return bool
* TRUE if the translation is outdated, FALSE otherwise.
*/
public function isOutdated();
/**
* Sets the translation outdated status.
*
* @param bool $outdated
* TRUE if the translation is outdated, FALSE otherwise.
*
* @return $this
*/
public function setOutdated($outdated);
/**
* Returns the translation author.
*
* @return \Drupal\user\UserInterface
* The user entity for the translation author.
*/
public function getAuthor();
/**
* Sets the translation author.
*
* @param \Drupal\user\UserInterface $account
* The translation author user entity.
*
* @return $this
*/
public function setAuthor(UserInterface $account);
/**
* Returns the translation published status.
*
* @return bool
* TRUE if the translation is published, FALSE otherwise.
*/
public function isPublished();
/**
* Sets the translation published status.
*
* @param bool $published
* TRUE if the translation is published, FALSE otherwise.
*
* @return $this
*/
public function setPublished($published);
/**
* Returns the translation creation timestamp.
*
* @return int
* The UNIX timestamp of when the translation was created.
*/
public function getCreatedTime();
/**
* Sets the translation creation timestamp.
*
* @param int $timestamp
* The UNIX timestamp of when the translation was created.
*
* @return $this
*/
public function setCreatedTime($timestamp);
/**
* Sets the translation modification timestamp.
*
* @param int $timestamp
* The UNIX timestamp of when the translation was last modified.
*
* @return $this
*/
public function setChangedTime($timestamp);
}
<?php
/**
* Contains \Drupal\content_translation\ContentTranslationUpdatesManager.
*/
namespace Drupal\content_translation;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides the logic needed to update field storage definitions when needed.
*/
class ContentTranslationUpdatesManager implements EventSubscriberInterface {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity definition update manager.
*
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
*/
protected $updateManager;
/**
* Constructs an updates manager instance.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $update_manager
* The entity definition update manager.
*/
public function __construct(EntityManagerInterface $entity_manager, EntityDefinitionUpdateManagerInterface $update_manager) {
$this->entityManager = $entity_manager;
$this->updateManager = $update_manager;
}
/**
* Executes field storage definition updates if needed.
*
* @param array $entity_types
* A list of entity type definitions to be processed.
*/
public function updateDefinitions(array $entity_types) {
// Handle field storage definition creation, if needed.
// @todo Generalize this code in https://www.drupal.org/node/2346013.
// @todo Handle initial values in https://www.drupal.org/node/2346019.
if ($this->updateManager->needsUpdates()) {
foreach ($entity_types as $entity_type_id => $entity_type) {
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
$installed_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
foreach (array_diff_key($storage_definitions, $installed_storage_definitions) as $storage_definition) {
/** @var $storage_definition \Drupal\Core\Field\FieldStorageDefinitionInterface */
if ($storage_definition->getProvider() == 'content_translation') {
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
}
}
}
}
}
/**
* Listener for the ConfigImporter import event.
*/